cleaned up web-app

This commit is contained in:
Boki 2025-07-04 22:25:23 -04:00
parent 805ce0ebf1
commit 3843dc95a3
21 changed files with 105 additions and 2420 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -46,15 +46,32 @@ export const BacktestChart = memo(function BacktestChart({ result, isLoading }:
// Find trades for this symbol
const symbolTrades = result.trades?.filter(t => t.symbol === symbol) || [];
console.log('Symbol trades for', symbol, ':', symbolTrades);
const tradeMarkers = symbolTrades
.filter(trade => trade.entryPrice != null && trade.entryDate != null)
.map(trade => ({
time: new Date(trade.entryDate).getTime() / 1000,
position: 'belowBar' as const,
color: trade.side === 'buy' ? '#10b981' : '#ef4444',
shape: trade.side === 'buy' ? 'arrowUp' : 'arrowDown' as const,
text: `${trade.side === 'buy' ? 'Buy' : 'Sell'} @ $${trade.entryPrice.toFixed(2)}`
}));
.filter(trade => {
// Check multiple possible field names
const hasPrice = trade.price != null || trade.entryPrice != null;
const hasTime = trade.timestamp != null || trade.entryDate != null || trade.date != null;
return hasPrice && hasTime;
})
.map(trade => {
// Use whatever field names are present
const price = trade.price || trade.entryPrice;
const timestamp = trade.timestamp || trade.entryDate || trade.date;
const side = trade.side || (trade.quantity > 0 ? 'buy' : 'sell');
return {
time: new Date(timestamp).getTime() / 1000,
position: 'belowBar' as const,
color: side === 'buy' ? '#10b981' : '#ef4444',
shape: (side === 'buy' ? 'arrowUp' : 'arrowDown') as const,
text: `${side === 'buy' ? 'Buy' : 'Sell'} @ $${price.toFixed(2)}`,
price: price
};
});
console.log('Trade markers:', tradeMarkers);
const processedOhlcData = ohlcData
.filter(d => d && d.timestamp != null && d.open != null && d.high != null && d.low != null && d.close != null)

View file

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

View file

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

View file

@ -3,7 +3,7 @@ import { DataTable } from '@/components/ui/DataTable';
import type { ColumnDef } from '@tanstack/react-table';
interface BacktestTradesProps {
result: BacktestResult | null;
result: any | null;
isLoading: boolean;
}
@ -28,7 +28,10 @@ export function BacktestTrades({ result, isLoading }: BacktestTradesProps) {
);
}
const columns: ColumnDef<typeof result.trades[0]>[] = [
// Add debug logging
console.log('Trades data:', result.trades);
const columns: ColumnDef<any>[] = [
{
accessorKey: 'symbol',
header: 'Symbol',
@ -38,11 +41,12 @@ export function BacktestTrades({ result, isLoading }: BacktestTradesProps) {
),
},
{
accessorKey: 'side',
id: 'side',
header: 'Side',
size: 80,
cell: ({ getValue }) => {
const side = getValue() as string || 'unknown';
cell: ({ row }) => {
const trade = row.original;
const side = trade.side || (trade.quantity > 0 ? 'buy' : 'sell');
return (
<span className={`inline-flex px-2 py-1 text-xs font-medium rounded-md ${
side === 'buy'
@ -55,11 +59,12 @@ export function BacktestTrades({ result, isLoading }: BacktestTradesProps) {
},
},
{
accessorKey: 'entryDate',
header: 'Entry Date',
id: 'date',
header: 'Date',
size: 180,
cell: ({ getValue }) => {
const date = getValue() as string;
cell: ({ row }) => {
const trade = row.original;
const date = trade.timestamp || trade.entryDate || trade.date;
return (
<span className="text-sm text-text-secondary">
{date ? new Date(date).toLocaleString() : '-'}
@ -68,37 +73,12 @@ export function BacktestTrades({ result, isLoading }: BacktestTradesProps) {
},
},
{
accessorKey: 'entryPrice',
header: 'Entry Price',
id: 'price',
header: 'Price',
size: 100,
cell: ({ getValue }) => {
const price = getValue() as number;
return (
<span className="text-sm text-text-primary">
${price != null ? price.toFixed(2) : '0.00'}
</span>
);
},
},
{
accessorKey: 'exitDate',
header: 'Exit Date',
size: 180,
cell: ({ getValue }) => {
const date = getValue() as string | null;
return (
<span className="text-sm text-text-secondary">
{date ? new Date(date).toLocaleString() : '-'}
</span>
);
},
},
{
accessorKey: 'exitPrice',
header: 'Exit Price',
size: 100,
cell: ({ getValue }) => {
const price = getValue() as number;
cell: ({ row }) => {
const trade = row.original;
const price = trade.price || trade.entryPrice;
return (
<span className="text-sm text-text-primary">
${price != null ? price.toFixed(2) : '0.00'}
@ -110,49 +90,83 @@ export function BacktestTrades({ result, isLoading }: BacktestTradesProps) {
accessorKey: 'quantity',
header: 'Quantity',
size: 80,
cell: ({ getValue }) => (
<span className="text-sm text-text-primary">{getValue() as number || 0}</span>
),
cell: ({ getValue }) => {
const qty = Math.abs(getValue() as number || 0);
return <span className="text-sm text-text-primary">{qty}</span>;
},
},
{
accessorKey: 'pnl',
header: 'P&L',
accessorKey: 'commission',
header: 'Commission',
size: 100,
cell: ({ getValue }) => {
const pnl = getValue() as number || 0;
const commission = getValue() as number || 0;
return (
<span className={`text-sm font-medium ${
pnl >= 0 ? 'text-success' : 'text-error'
}`}>
${pnl != null ? pnl.toFixed(2) : '0.00'}
<span className="text-sm text-text-secondary">
${commission.toFixed(2)}
</span>
);
},
},
{
accessorKey: 'pnlPercent',
header: 'P&L %',
id: 'pnl',
header: 'P&L',
size: 100,
cell: ({ getValue }) => {
const pnlPercent = getValue() as number || 0;
cell: ({ row }) => {
const trade = row.original;
const pnl = trade.pnl || trade.realizedPnl || 0;
return (
<span className={`text-sm font-medium ${
pnlPercent >= 0 ? 'text-success' : 'text-error'
pnl >= 0 ? 'text-success' : 'text-error'
}`}>
{pnlPercent != null ? pnlPercent.toFixed(2) : '0.00'}%
${pnl.toFixed(2)}
</span>
);
},
},
];
// Calculate trade statistics
const tradeStats = result.trades.reduce((stats: any, trade: any) => {
const pnl = trade.pnl || trade.realizedPnl || 0;
if (pnl > 0) {
stats.wins++;
stats.totalWin += pnl;
} else if (pnl < 0) {
stats.losses++;
stats.totalLoss += Math.abs(pnl);
}
stats.totalPnL += pnl;
return stats;
}, { wins: 0, losses: 0, totalWin: 0, totalLoss: 0, totalPnL: 0 });
const winRate = result.trades.length > 0 ? (tradeStats.wins / result.trades.length) * 100 : 0;
return (
<div>
<div className="mb-4">
<h3 className="text-base font-medium text-text-primary">Trade History</h3>
<p className="text-sm text-text-secondary mt-1">
Total: {result.trades.length} trades
</p>
<div className="mb-4 grid grid-cols-1 md:grid-cols-5 gap-4">
<div className="bg-surface-secondary rounded-lg border border-border p-3">
<p className="text-xs text-text-secondary">Total Trades</p>
<p className="text-lg font-medium text-text-primary">{result.trades.length}</p>
</div>
<div className="bg-surface-secondary rounded-lg border border-border p-3">
<p className="text-xs text-text-secondary">Win Rate</p>
<p className="text-lg font-medium text-text-primary">{winRate.toFixed(1)}%</p>
</div>
<div className="bg-surface-secondary rounded-lg border border-border p-3">
<p className="text-xs text-text-secondary">Wins</p>
<p className="text-lg font-medium text-success">{tradeStats.wins}</p>
</div>
<div className="bg-surface-secondary rounded-lg border border-border p-3">
<p className="text-xs text-text-secondary">Losses</p>
<p className="text-lg font-medium text-error">{tradeStats.losses}</p>
</div>
<div className="bg-surface-secondary rounded-lg border border-border p-3">
<p className="text-xs text-text-secondary">Total P&L</p>
<p className={`text-lg font-medium ${tradeStats.totalPnL >= 0 ? 'text-success' : 'text-error'}`}>
${tradeStats.totalPnL.toFixed(2)}
</p>
</div>
</div>
<DataTable

View file

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

View file

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

View file

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

View file

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

View file

@ -1,12 +1,11 @@
export { BacktestConfiguration } from './BacktestConfiguration';
export { BacktestControls } from './BacktestControls';
export { BacktestResults } from './BacktestResults';
export { MetricsCard } from './MetricsCard';
export { PositionsTable } from './PositionsTable';
export { TradeLog } from './TradeLog';
export { RunsList } from './RunsList';
export { RunsListWithMetrics } from './RunsListWithMetrics';
export { CompactPerformanceMetrics } from './CompactPerformanceMetrics';
export { CompactPositionsTable } from './CompactPositionsTable';
export { PositionsSummary } from './PositionsSummary';
export { ChartContainer } from './ChartContainer';
export { ChartContainer } from './ChartContainer';
export { BacktestChart } from './BacktestChart';
export { BacktestMetrics } from './BacktestMetrics';
export { BacktestPlayback } from './BacktestPlayback';
export { BacktestTrades } from './BacktestTrades';
export { RunControlsCompact } from './RunControlsCompact';

View file

@ -1 +0,0 @@
export { useBacktest } from './useBacktest';

View file

@ -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,
};
}

View file

@ -1,6 +1,3 @@
export { BacktestPage } from './BacktestPage';
export { BacktestListPage } from './BacktestListPage';
export { BacktestDetailPage } from './BacktestDetailPage';
export { BacktestListPageV2 } from './BacktestListPageV2';
export { BacktestDetailPageV2 } from './BacktestDetailPageV2';
export * from './types';

View file

@ -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}`);
}
},
};

View file

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

View file

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

View file

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

View file

@ -1,7 +1,5 @@
export { SymbolChart } from './SymbolChart';
export { ChartToolbar } from './ChartToolbar';
export { MarketOverview } from './MarketOverview';
export { ChartErrorBoundary } from './ChartErrorBoundary';
export { IndicatorSelector } from './IndicatorSelector';
export { IndicatorList } from './IndicatorList';
export { IndicatorSettings } from './IndicatorSettings';