initial charts / backtest
This commit is contained in:
parent
11c24b2280
commit
1b9010ebf4
37 changed files with 3888 additions and 23 deletions
69
apps/stock/web-app/src/features/backtest/BacktestPage.tsx
Normal file
69
apps/stock/web-app/src/features/backtest/BacktestPage.tsx
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { BacktestConfiguration } from './components/BacktestConfiguration';
|
||||
import { BacktestResults } from './components/BacktestResults';
|
||||
import { BacktestControls } from './components/BacktestControls';
|
||||
import { useBacktest } from './hooks/useBacktest';
|
||||
|
||||
export function BacktestPage() {
|
||||
const {
|
||||
config,
|
||||
status,
|
||||
results,
|
||||
currentTime,
|
||||
error,
|
||||
isLoading,
|
||||
handleConfigSubmit,
|
||||
handleStart,
|
||||
handlePause,
|
||||
handleResume,
|
||||
handleStop,
|
||||
handleStep,
|
||||
} = useBacktest();
|
||||
|
||||
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}
|
||||
/>
|
||||
|
||||
{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={results}
|
||||
currentTime={currentTime}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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';
|
||||
1
apps/stock/web-app/src/features/backtest/hooks/index.ts
Normal file
1
apps/stock/web-app/src/features/backtest/hooks/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { useBacktest } from './useBacktest';
|
||||
169
apps/stock/web-app/src/features/backtest/hooks/useBacktest.ts
Normal file
169
apps/stock/web-app/src/features/backtest/hooks/useBacktest.ts
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { BacktestService } from '../services/backtestService';
|
||||
import type { BacktestConfig, BacktestResult, BacktestStatus } from '../types';
|
||||
|
||||
export function useBacktest() {
|
||||
const [backtestId, setBacktestId] = useState<string | null>(null);
|
||||
const [config, setConfig] = useState<BacktestConfig | null>(null);
|
||||
const [status, setStatus] = useState<BacktestStatus>('idle');
|
||||
const [results, setResults] = useState<BacktestResult | null>(null);
|
||||
const [currentTime, setCurrentTime] = useState<number | null>(null);
|
||||
const [progress, setProgress] = useState<number>(0);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const cleanupRef = useRef<(() => void) | null>(null);
|
||||
|
||||
const handleConfigSubmit = useCallback(async (newConfig: BacktestConfig) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Create backtest
|
||||
const { id } = await BacktestService.createBacktest(newConfig);
|
||||
|
||||
setBacktestId(id);
|
||||
setConfig(newConfig);
|
||||
setStatus('configured');
|
||||
setResults(null);
|
||||
setCurrentTime(null);
|
||||
setProgress(0);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to create backtest');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleStart = useCallback(async () => {
|
||||
if (!backtestId) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
await BacktestService.startBacktest(backtestId);
|
||||
setStatus('running');
|
||||
|
||||
// Start polling for updates
|
||||
cleanupRef.current = await BacktestService.pollBacktestUpdates(
|
||||
backtestId,
|
||||
(newStatus, newProgress, newTime) => {
|
||||
setStatus(newStatus);
|
||||
if (newProgress !== undefined) setProgress(newProgress);
|
||||
if (newTime !== undefined) setCurrentTime(newTime);
|
||||
|
||||
// Fetch full results when completed
|
||||
if (newStatus === 'completed') {
|
||||
BacktestService.getBacktestResults(backtestId).then(setResults);
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to start backtest');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [backtestId]);
|
||||
|
||||
const handlePause = useCallback(async () => {
|
||||
if (!backtestId) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
await BacktestService.pauseBacktest(backtestId);
|
||||
setStatus('paused');
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to pause backtest');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [backtestId]);
|
||||
|
||||
const handleResume = useCallback(async () => {
|
||||
if (!backtestId) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
await BacktestService.resumeBacktest(backtestId);
|
||||
setStatus('running');
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to resume backtest');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [backtestId]);
|
||||
|
||||
const handleStop = useCallback(async () => {
|
||||
if (!backtestId) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
await BacktestService.stopBacktest(backtestId);
|
||||
setStatus('stopped');
|
||||
|
||||
// Stop polling
|
||||
if (cleanupRef.current) {
|
||||
cleanupRef.current();
|
||||
cleanupRef.current = null;
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to stop backtest');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [backtestId]);
|
||||
|
||||
const handleStep = useCallback(async () => {
|
||||
if (!backtestId) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
await BacktestService.stepBacktest(backtestId);
|
||||
|
||||
// Get updated status
|
||||
const statusUpdate = await BacktestService.getBacktestStatus(backtestId);
|
||||
setStatus(statusUpdate.status);
|
||||
if (statusUpdate.progress !== undefined) setProgress(statusUpdate.progress);
|
||||
if (statusUpdate.currentTime !== undefined) setCurrentTime(statusUpdate.currentTime);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to step backtest');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [backtestId]);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (cleanupRef.current) {
|
||||
cleanupRef.current();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
backtestId,
|
||||
config,
|
||||
status,
|
||||
results,
|
||||
currentTime,
|
||||
progress,
|
||||
error,
|
||||
isLoading,
|
||||
handleConfigSubmit,
|
||||
handleStart,
|
||||
handlePause,
|
||||
handleResume,
|
||||
handleStop,
|
||||
handleStep,
|
||||
};
|
||||
}
|
||||
2
apps/stock/web-app/src/features/backtest/index.ts
Normal file
2
apps/stock/web-app/src/features/backtest/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { BacktestPage } from './BacktestPage';
|
||||
export * from './types';
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
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 = 1000
|
||||
): 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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
export type BacktestStatus =
|
||||
| 'idle'
|
||||
| 'configured'
|
||||
| 'running'
|
||||
| 'paused'
|
||||
| 'stopped'
|
||||
| 'completed'
|
||||
| 'error';
|
||||
|
||||
export interface BacktestConfig {
|
||||
name: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
initialCapital: number;
|
||||
symbols: string[];
|
||||
strategy: string;
|
||||
speedMultiplier: number;
|
||||
commission: number;
|
||||
slippage: number;
|
||||
}
|
||||
|
||||
export interface BacktestMetrics {
|
||||
totalReturn: number;
|
||||
sharpeRatio: number;
|
||||
maxDrawdown: number;
|
||||
winRate: number;
|
||||
totalTrades: number;
|
||||
profitableTrades: number;
|
||||
}
|
||||
|
||||
export interface Position {
|
||||
symbol: string;
|
||||
quantity: number;
|
||||
averagePrice: number;
|
||||
currentPrice: number;
|
||||
realizedPnl: number;
|
||||
unrealizedPnl: number;
|
||||
lastUpdate: string;
|
||||
}
|
||||
|
||||
export interface Trade {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
symbol: string;
|
||||
side: 'buy' | 'sell';
|
||||
quantity: number;
|
||||
price: number;
|
||||
commission: number;
|
||||
pnl?: number;
|
||||
}
|
||||
|
||||
export interface PerformanceDataPoint {
|
||||
timestamp: string;
|
||||
portfolioValue: number;
|
||||
pnl: number;
|
||||
drawdown: number;
|
||||
}
|
||||
|
||||
export interface BacktestResult {
|
||||
id: string;
|
||||
config: BacktestConfig;
|
||||
metrics: BacktestMetrics;
|
||||
positions: Position[];
|
||||
trades: Trade[];
|
||||
performanceData: PerformanceDataPoint[];
|
||||
}
|
||||
|
||||
export interface OrderBookLevel {
|
||||
price: number;
|
||||
size: number;
|
||||
orderCount?: number;
|
||||
}
|
||||
|
||||
export interface OrderBookSnapshot {
|
||||
symbol: string;
|
||||
timestamp: string;
|
||||
bids: OrderBookLevel[];
|
||||
asks: OrderBookLevel[];
|
||||
}
|
||||
|
||||
export interface RiskMetrics {
|
||||
currentExposure: number;
|
||||
dailyPnl: number;
|
||||
positionCount: number;
|
||||
grossExposure: number;
|
||||
maxPositionSize: number;
|
||||
utilizationPct: number;
|
||||
}
|
||||
12
apps/stock/web-app/src/features/backtest/types/index.ts
Normal file
12
apps/stock/web-app/src/features/backtest/types/index.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export type {
|
||||
BacktestStatus,
|
||||
BacktestConfig,
|
||||
BacktestMetrics,
|
||||
Position,
|
||||
Trade,
|
||||
PerformanceDataPoint,
|
||||
BacktestResult,
|
||||
OrderBookLevel,
|
||||
OrderBookSnapshot,
|
||||
RiskMetrics,
|
||||
} from './backtest.types';
|
||||
Loading…
Add table
Add a link
Reference in a new issue