initial charts / backtest
This commit is contained in:
parent
11c24b2280
commit
1b9010ebf4
37 changed files with 3888 additions and 23 deletions
|
|
@ -0,0 +1,257 @@
|
|||
import { useState } from 'react';
|
||||
import type { BacktestConfig } from '../types';
|
||||
|
||||
interface BacktestConfigurationProps {
|
||||
onSubmit: (config: BacktestConfig) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function BacktestConfiguration({ onSubmit, disabled }: BacktestConfigurationProps) {
|
||||
const [formData, setFormData] = useState<BacktestConfig>({
|
||||
name: '',
|
||||
startDate: new Date(new Date().setMonth(new Date().getMonth() - 6)), // 6 months ago
|
||||
endDate: new Date(),
|
||||
initialCapital: 100000,
|
||||
symbols: [],
|
||||
strategy: 'momentum',
|
||||
speedMultiplier: 1,
|
||||
commission: 0.001, // 0.1%
|
||||
slippage: 0.0005, // 0.05%
|
||||
});
|
||||
|
||||
const [symbolInput, setSymbolInput] = useState('');
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (formData.symbols.length === 0) {
|
||||
alert('Please add at least one symbol');
|
||||
return;
|
||||
}
|
||||
onSubmit(formData);
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const { name, value, type } = e.target;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: type === 'number' ? parseFloat(value) :
|
||||
type === 'date' ? new Date(value) : value
|
||||
}));
|
||||
};
|
||||
|
||||
const handleAddSymbol = () => {
|
||||
const symbol = symbolInput.trim().toUpperCase();
|
||||
if (symbol && !formData.symbols.includes(symbol)) {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
symbols: [...prev.symbols, symbol]
|
||||
}));
|
||||
setSymbolInput('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveSymbol = (symbol: string) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
symbols: prev.symbols.filter(s => s !== symbol)
|
||||
}));
|
||||
};
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toISOString().split('T')[0];
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-surface-secondary p-4 rounded-lg border border-border">
|
||||
<h2 className="text-base font-medium text-text-primary mb-4">Configuration</h2>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-1">
|
||||
Backtest Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
placeholder="My Strategy Backtest"
|
||||
className="w-full px-3 py-2 bg-background border border-border rounded-md text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||
disabled={disabled}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-1">
|
||||
Start Date
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
name="startDate"
|
||||
value={formatDate(formData.startDate)}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 bg-background border border-border rounded-md text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||
disabled={disabled}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-1">
|
||||
End Date
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
name="endDate"
|
||||
value={formatDate(formData.endDate)}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 bg-background border border-border rounded-md text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||
disabled={disabled}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-1">
|
||||
Initial Capital ($)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="initialCapital"
|
||||
value={formData.initialCapital}
|
||||
onChange={handleChange}
|
||||
min="1000"
|
||||
step="1000"
|
||||
className="w-full px-3 py-2 bg-background border border-border rounded-md text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||
disabled={disabled}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-1">
|
||||
Symbols
|
||||
</label>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<input
|
||||
type="text"
|
||||
value={symbolInput}
|
||||
onChange={(e) => setSymbolInput(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), handleAddSymbol())}
|
||||
placeholder="AAPL"
|
||||
className="flex-1 px-3 py-2 bg-background border border-border rounded-md text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||
disabled={disabled}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddSymbol}
|
||||
className="px-4 py-2 bg-primary-500 text-white rounded-md text-sm font-medium hover:bg-primary-600 transition-colors disabled:opacity-50"
|
||||
disabled={disabled}
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{formData.symbols.map(symbol => (
|
||||
<span
|
||||
key={symbol}
|
||||
className="inline-flex items-center gap-1 px-2 py-1 bg-primary-500/10 text-primary-400 rounded-md text-sm"
|
||||
>
|
||||
{symbol}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveSymbol(symbol)}
|
||||
className="text-primary-400 hover:text-primary-300"
|
||||
disabled={disabled}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-1">
|
||||
Strategy
|
||||
</label>
|
||||
<select
|
||||
name="strategy"
|
||||
value={formData.strategy}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 bg-background border border-border rounded-md text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||
disabled={disabled}
|
||||
>
|
||||
<option value="momentum">Momentum</option>
|
||||
<option value="mean-reversion">Mean Reversion</option>
|
||||
<option value="pairs-trading">Pairs Trading</option>
|
||||
<option value="custom">Custom</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-1">
|
||||
Commission (%)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="commission"
|
||||
value={formData.commission * 100}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, commission: parseFloat(e.target.value) / 100 }))}
|
||||
min="0"
|
||||
step="0.01"
|
||||
className="w-full px-3 py-2 bg-background border border-border rounded-md text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-1">
|
||||
Slippage (%)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="slippage"
|
||||
value={formData.slippage * 100}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, slippage: parseFloat(e.target.value) / 100 }))}
|
||||
min="0"
|
||||
step="0.01"
|
||||
className="w-full px-3 py-2 bg-background border border-border rounded-md text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-1">
|
||||
Speed Multiplier
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="speedMultiplier"
|
||||
value={formData.speedMultiplier}
|
||||
onChange={handleChange}
|
||||
min="0.1"
|
||||
max="1000"
|
||||
step="0.1"
|
||||
className="w-full px-3 py-2 bg-background border border-border rounded-md text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||
disabled={disabled}
|
||||
/>
|
||||
<p className="text-xs text-text-muted mt-1">1x = real-time, 10x = 10x faster</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full px-4 py-2 bg-primary-500 text-white rounded-md text-sm font-medium hover:bg-primary-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled={disabled}
|
||||
>
|
||||
Configure Backtest
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
import type { BacktestResult, BacktestStatus } from '../types';
|
||||
import { MetricsCard } from './MetricsCard';
|
||||
import { PositionsTable } from './PositionsTable';
|
||||
import { TradeLog } from './TradeLog';
|
||||
|
||||
interface BacktestResultsProps {
|
||||
status: BacktestStatus;
|
||||
results: BacktestResult | null;
|
||||
currentTime: number | null;
|
||||
}
|
||||
|
||||
export function BacktestResults({ status, results, currentTime }: BacktestResultsProps) {
|
||||
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 ? '+' : ''}${results.metrics.totalReturn.toFixed(2)}%`}
|
||||
trend={results.metrics.totalReturn >= 0 ? 'up' : 'down'}
|
||||
/>
|
||||
<MetricsCard
|
||||
title="Sharpe Ratio"
|
||||
value={results.metrics.sharpeRatio.toFixed(2)}
|
||||
trend={results.metrics.sharpeRatio >= 1 ? 'up' : 'down'}
|
||||
/>
|
||||
<MetricsCard
|
||||
title="Max Drawdown"
|
||||
value={`${results.metrics.maxDrawdown.toFixed(2)}%`}
|
||||
trend="down"
|
||||
/>
|
||||
<MetricsCard
|
||||
title="Win Rate"
|
||||
value={`${results.metrics.winRate.toFixed(1)}%`}
|
||||
trend={results.metrics.winRate >= 50 ? 'up' : 'down'}
|
||||
/>
|
||||
<MetricsCard
|
||||
title="Total Trades"
|
||||
value={results.metrics.totalTrades.toString()}
|
||||
/>
|
||||
<MetricsCard
|
||||
title="Profitable Trades"
|
||||
value={results.metrics.profitableTrades.toString()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Performance Chart Placeholder */}
|
||||
<div className="bg-surface-secondary p-4 rounded-lg border border-border">
|
||||
<h3 className="text-base font-medium text-text-primary mb-4">
|
||||
Portfolio Performance
|
||||
</h3>
|
||||
<div className="h-64 bg-background rounded border border-border flex items-center justify-center">
|
||||
<p className="text-sm text-text-muted">
|
||||
Performance chart will be displayed here (requires recharts)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Positions Table */}
|
||||
{results.positions.length > 0 && (
|
||||
<div className="bg-surface-secondary p-4 rounded-lg border border-border">
|
||||
<h3 className="text-base font-medium text-text-primary mb-4">
|
||||
Current Positions
|
||||
</h3>
|
||||
<PositionsTable positions={results.positions} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Trade Log */}
|
||||
{results.trades.length > 0 && (
|
||||
<div className="bg-surface-secondary p-4 rounded-lg border border-border">
|
||||
<h3 className="text-base font-medium text-text-primary mb-4">
|
||||
Trade History
|
||||
</h3>
|
||||
<TradeLog trades={results.trades} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import { ArrowUpIcon, ArrowDownIcon } from '@heroicons/react/24/solid';
|
||||
|
||||
interface MetricsCardProps {
|
||||
title: string;
|
||||
value: string;
|
||||
trend?: 'up' | 'down';
|
||||
subtitle?: string;
|
||||
}
|
||||
|
||||
export function MetricsCard({ title, value, trend, subtitle }: MetricsCardProps) {
|
||||
return (
|
||||
<div className="bg-background p-4 rounded-lg border border-border">
|
||||
<h4 className="text-sm font-medium text-text-secondary mb-1">{title}</h4>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className={`text-2xl font-bold ${
|
||||
trend === 'up' ? 'text-success' :
|
||||
trend === 'down' ? 'text-error' :
|
||||
'text-text-primary'
|
||||
}`}>
|
||||
{value}
|
||||
</p>
|
||||
{trend && (
|
||||
<span className={`inline-flex ${
|
||||
trend === 'up' ? 'text-success' : 'text-error'
|
||||
}`}>
|
||||
{trend === 'up' ?
|
||||
<ArrowUpIcon className="w-4 h-4" /> :
|
||||
<ArrowDownIcon className="w-4 h-4" />
|
||||
}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{subtitle && (
|
||||
<p className="text-xs text-text-muted mt-1">{subtitle}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
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();
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto max-h-96">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="sticky top-0 bg-surface-secondary">
|
||||
<tr className="border-b border-border">
|
||||
<th className="text-left py-2 px-2 font-medium text-text-secondary">Time</th>
|
||||
<th className="text-left py-2 px-2 font-medium text-text-secondary">Symbol</th>
|
||||
<th className="text-center py-2 px-2 font-medium text-text-secondary">Side</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">Price</th>
|
||||
<th className="text-right py-2 px-2 font-medium text-text-secondary">Value</th>
|
||||
<th className="text-right py-2 px-2 font-medium text-text-secondary">Comm.</th>
|
||||
{trades.some(t => t.pnl !== undefined) && (
|
||||
<th className="text-right py-2 px-2 font-medium text-text-secondary">P&L</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortedTrades.map((trade) => {
|
||||
const tradeValue = trade.quantity * trade.price;
|
||||
return (
|
||||
<tr key={trade.id} className="border-b border-border hover:bg-surface-tertiary">
|
||||
<td className="py-2 px-2 text-text-muted text-xs">
|
||||
{formatTime(trade.timestamp)}
|
||||
</td>
|
||||
<td className="py-2 px-2 font-medium text-text-primary">{trade.symbol}</td>
|
||||
<td className="text-center py-2 px-2">
|
||||
<span className={`inline-flex px-2 py-0.5 text-xs font-medium rounded ${
|
||||
trade.side === 'buy'
|
||||
? 'bg-success/10 text-success'
|
||||
: 'bg-error/10 text-error'
|
||||
}`}>
|
||||
{trade.side.toUpperCase()}
|
||||
</span>
|
||||
</td>
|
||||
<td className="text-right py-2 px-2 text-text-primary">
|
||||
{trade.quantity.toLocaleString()}
|
||||
</td>
|
||||
<td className="text-right py-2 px-2 text-text-primary">
|
||||
{formatCurrency(trade.price)}
|
||||
</td>
|
||||
<td className="text-right py-2 px-2 text-text-primary">
|
||||
{formatCurrency(tradeValue)}
|
||||
</td>
|
||||
<td className="text-right py-2 px-2 text-text-muted">
|
||||
{formatCurrency(trade.commission)}
|
||||
</td>
|
||||
{trade.pnl !== undefined && (
|
||||
<td className={`text-right py-2 px-2 font-medium ${
|
||||
trade.pnl >= 0 ? 'text-success' : 'text-error'
|
||||
}`}>
|
||||
{trade.pnl >= 0 ? '+' : ''}{formatCurrency(trade.pnl)}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export { BacktestConfiguration } from './BacktestConfiguration';
|
||||
export { BacktestControls } from './BacktestControls';
|
||||
export { BacktestResults } from './BacktestResults';
|
||||
export { MetricsCard } from './MetricsCard';
|
||||
export { PositionsTable } from './PositionsTable';
|
||||
export { TradeLog } from './TradeLog';
|
||||
Loading…
Add table
Add a link
Reference in a new issue