initial charts / backtest

This commit is contained in:
Boki 2025-07-02 19:58:43 -04:00
parent 11c24b2280
commit 1b9010ebf4
37 changed files with 3888 additions and 23 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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