adding backtest table / pages
This commit is contained in:
parent
38a6e73ad5
commit
a876f3c35b
19 changed files with 1058 additions and 69 deletions
Binary file not shown.
|
|
@ -71,6 +71,12 @@ async function createContainer(config: any) {
|
|||
})
|
||||
.build(); // This automatically initializes services
|
||||
|
||||
// Run database migrations
|
||||
if (container.postgres) {
|
||||
const { runMigrations } = await import('./migrations/migration-runner');
|
||||
await runMigrations(container);
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
|
|
|
|||
70
apps/stock/web-api/src/migrations/migration-runner.ts
Normal file
70
apps/stock/web-api/src/migrations/migration-runner.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { readFileSync, readdirSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import type { IServiceContainer } from '@stock-bot/handlers';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
|
||||
const logger = getLogger('migration-runner');
|
||||
|
||||
export async function runMigrations(container: IServiceContainer): Promise<void> {
|
||||
logger.info('Migration runner called');
|
||||
logger.info('Container type:', typeof container);
|
||||
logger.info('Container postgres available:', !!container.postgres);
|
||||
|
||||
if (!container.postgres) {
|
||||
logger.warn('PostgreSQL not available, skipping migrations');
|
||||
logger.info('Container keys:', Object.keys(container));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Create migrations table if it doesn't exist
|
||||
await container.postgres.query(`
|
||||
CREATE TABLE IF NOT EXISTS migrations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
filename VARCHAR(255) NOT NULL UNIQUE,
|
||||
executed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
||||
)
|
||||
`);
|
||||
|
||||
// Get list of migration files from database/postgres/init
|
||||
const migrationsDir = join(process.cwd(), 'database', 'postgres', 'init');
|
||||
logger.info('Looking for migrations in:', migrationsDir);
|
||||
|
||||
const files = readdirSync(migrationsDir)
|
||||
.filter(f => f.endsWith('.sql') && f.startsWith('001_')) // Only run our backtest migration for now
|
||||
.sort();
|
||||
|
||||
logger.info('Found migration files:', files);
|
||||
|
||||
for (const file of files) {
|
||||
// Check if migration has already been run
|
||||
const result = await container.postgres.query(
|
||||
'SELECT 1 FROM migrations WHERE filename = $1',
|
||||
[file]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
logger.info(`Running migration: ${file}`);
|
||||
|
||||
// Read and execute migration
|
||||
const sql = readFileSync(join(migrationsDir, file), 'utf8');
|
||||
await container.postgres.query(sql);
|
||||
|
||||
// Record migration as executed
|
||||
await container.postgres.query(
|
||||
'INSERT INTO migrations (filename) VALUES ($1)',
|
||||
[file]
|
||||
);
|
||||
|
||||
logger.info(`Migration completed: ${file}`);
|
||||
} else {
|
||||
logger.debug(`Migration already executed: ${file}`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('All migrations completed successfully');
|
||||
} catch (error) {
|
||||
logger.error('Migration failed', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ const logger = getLogger('backtest-routes');
|
|||
|
||||
export function createBacktestRoutes(container: IServiceContainer) {
|
||||
const backtestRoutes = new Hono();
|
||||
const backtestService = new BacktestService();
|
||||
const backtestService = new BacktestService(container);
|
||||
|
||||
// Create a new backtest
|
||||
backtestRoutes.post('/api/backtests', async (c) => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import type { IServiceContainer } from '@stock-bot/handlers';
|
||||
|
||||
const logger = getLogger('backtest-service');
|
||||
|
||||
|
|
@ -29,29 +30,49 @@ export interface BacktestJob {
|
|||
error?: string;
|
||||
}
|
||||
|
||||
// In-memory storage for demo (replace with database)
|
||||
const backtestStore = new Map<string, BacktestJob>();
|
||||
const backtestResults = new Map<string, any>();
|
||||
|
||||
export class BacktestService {
|
||||
private container: IServiceContainer;
|
||||
|
||||
constructor(container: IServiceContainer) {
|
||||
this.container = container;
|
||||
logger.info('BacktestService initialized', {
|
||||
hasPostgres: !!container.postgres,
|
||||
containerKeys: Object.keys(container)
|
||||
});
|
||||
}
|
||||
async createBacktest(request: BacktestRequest): Promise<BacktestJob> {
|
||||
const backtestId = uuidv4();
|
||||
|
||||
// Store in memory (replace with database)
|
||||
const backtest: BacktestJob = {
|
||||
id: backtestId,
|
||||
status: 'pending',
|
||||
strategy: request.strategy,
|
||||
symbols: request.symbols,
|
||||
startDate: new Date(request.startDate),
|
||||
endDate: new Date(request.endDate),
|
||||
initialCapital: request.initialCapital,
|
||||
config: request.config || {},
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
// Insert into PostgreSQL
|
||||
const result = await this.container.postgres.query(
|
||||
`INSERT INTO backtests
|
||||
(id, name, strategy, symbols, start_date, end_date, initial_capital, config, status)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'pending')
|
||||
RETURNING *`,
|
||||
[
|
||||
backtestId,
|
||||
request.config?.name || `Backtest ${new Date().toISOString()}`,
|
||||
request.strategy,
|
||||
JSON.stringify(request.symbols),
|
||||
request.startDate,
|
||||
request.endDate,
|
||||
request.initialCapital,
|
||||
JSON.stringify(request.config || {})
|
||||
]
|
||||
);
|
||||
|
||||
backtestStore.set(backtestId, backtest);
|
||||
const backtest: BacktestJob = {
|
||||
id: result.rows[0].id,
|
||||
status: result.rows[0].status,
|
||||
strategy: result.rows[0].strategy,
|
||||
symbols: result.rows[0].symbols,
|
||||
startDate: result.rows[0].start_date,
|
||||
endDate: result.rows[0].end_date,
|
||||
initialCapital: parseFloat(result.rows[0].initial_capital),
|
||||
config: result.rows[0].config,
|
||||
createdAt: result.rows[0].created_at,
|
||||
updatedAt: result.rows[0].updated_at,
|
||||
};
|
||||
|
||||
// Call orchestrator to run backtest
|
||||
try {
|
||||
|
|
@ -87,10 +108,14 @@ export class BacktestService {
|
|||
|
||||
// Store result directly without transformation
|
||||
if (result.status === 'completed') {
|
||||
backtest.status = 'completed';
|
||||
backtest.updatedAt = new Date();
|
||||
backtestStore.set(backtestId, backtest);
|
||||
backtestResults.set(backtestId, result);
|
||||
// Update backtest status
|
||||
await this.container.postgres.query(
|
||||
'UPDATE backtests SET status = $1, updated_at = NOW() WHERE id = $2',
|
||||
['completed', backtestId]
|
||||
);
|
||||
|
||||
// Store results in backtest_results table
|
||||
await this.saveBacktestResults(backtestId, result);
|
||||
|
||||
logger.info('Backtest completed', {
|
||||
backtestId,
|
||||
|
|
@ -99,14 +124,19 @@ export class BacktestService {
|
|||
});
|
||||
} else {
|
||||
// Update status to running if not completed
|
||||
backtest.status = 'running';
|
||||
backtest.updatedAt = new Date();
|
||||
backtestStore.set(backtestId, backtest);
|
||||
await this.container.postgres.query(
|
||||
'UPDATE backtests SET status = $1, updated_at = NOW() WHERE id = $2',
|
||||
['running', backtestId]
|
||||
);
|
||||
}
|
||||
|
||||
logger.info('Backtest started in orchestrator', { backtestId, result });
|
||||
} catch (error) {
|
||||
logger.error('Failed to start backtest in orchestrator', { backtestId, error });
|
||||
await this.container.postgres.query(
|
||||
'UPDATE backtests SET status = $1, error = $2, updated_at = NOW() WHERE id = $3',
|
||||
['failed', error.message, backtestId]
|
||||
);
|
||||
backtest.status = 'failed';
|
||||
backtest.error = error.message;
|
||||
}
|
||||
|
|
@ -115,30 +145,113 @@ export class BacktestService {
|
|||
}
|
||||
|
||||
async getBacktest(id: string): Promise<BacktestJob | null> {
|
||||
return backtestStore.get(id) || null;
|
||||
const result = await this.container.postgres.query(
|
||||
'SELECT * FROM backtests WHERE id = $1',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const row = result.rows[0];
|
||||
return {
|
||||
id: row.id,
|
||||
status: row.status,
|
||||
strategy: row.strategy,
|
||||
symbols: row.symbols,
|
||||
startDate: row.start_date,
|
||||
endDate: row.end_date,
|
||||
initialCapital: parseFloat(row.initial_capital),
|
||||
config: row.config,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
error: row.error,
|
||||
};
|
||||
}
|
||||
|
||||
async getBacktestResults(id: string): Promise<any> {
|
||||
// Return results directly without any transformation
|
||||
return backtestResults.get(id) || null;
|
||||
const result = await this.container.postgres.query(
|
||||
`SELECT
|
||||
br.*,
|
||||
b.strategy,
|
||||
b.symbols,
|
||||
b.start_date,
|
||||
b.end_date,
|
||||
b.initial_capital,
|
||||
b.config as backtest_config
|
||||
FROM backtest_results br
|
||||
JOIN backtests b ON b.id = br.backtest_id
|
||||
WHERE br.backtest_id = $1`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const row = result.rows[0];
|
||||
|
||||
// Reconstruct the result format expected by frontend
|
||||
return {
|
||||
backtestId: row.backtest_id,
|
||||
status: 'completed',
|
||||
completedAt: row.completed_at.toISOString(),
|
||||
config: {
|
||||
name: row.backtest_config?.name || 'Backtest',
|
||||
strategy: row.strategy,
|
||||
symbols: row.symbols,
|
||||
startDate: row.start_date.toISOString(),
|
||||
endDate: row.end_date.toISOString(),
|
||||
initialCapital: parseFloat(row.initial_capital),
|
||||
commission: row.backtest_config?.commission ?? 0.001,
|
||||
slippage: row.backtest_config?.slippage ?? 0.0001,
|
||||
dataFrequency: row.backtest_config?.dataFrequency || '1d',
|
||||
},
|
||||
metrics: row.metrics,
|
||||
equity: row.equity_curve,
|
||||
ohlcData: row.ohlc_data,
|
||||
trades: row.trades,
|
||||
positions: row.positions,
|
||||
analytics: row.analytics,
|
||||
executionTime: row.execution_time,
|
||||
};
|
||||
}
|
||||
|
||||
async listBacktests(params: { limit: number; offset: number }): Promise<BacktestJob[]> {
|
||||
const all = Array.from(backtestStore.values());
|
||||
return all
|
||||
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
|
||||
.slice(params.offset, params.offset + params.limit);
|
||||
const result = await this.container.postgres.query(
|
||||
`SELECT * FROM backtests
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $1 OFFSET $2`,
|
||||
[params.limit, params.offset]
|
||||
);
|
||||
|
||||
return result.rows.map(row => ({
|
||||
id: row.id,
|
||||
status: row.status,
|
||||
strategy: row.strategy,
|
||||
symbols: row.symbols,
|
||||
startDate: row.start_date,
|
||||
endDate: row.end_date,
|
||||
initialCapital: parseFloat(row.initial_capital),
|
||||
config: row.config,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
error: row.error,
|
||||
}));
|
||||
}
|
||||
|
||||
async updateBacktestStatus(id: string, status: BacktestJob['status'], error?: string): Promise<void> {
|
||||
const backtest = backtestStore.get(id);
|
||||
if (backtest) {
|
||||
backtest.status = status;
|
||||
backtest.updatedAt = new Date();
|
||||
if (error) {
|
||||
backtest.error = error;
|
||||
}
|
||||
backtestStore.set(id, backtest);
|
||||
if (error) {
|
||||
await this.container.postgres.query(
|
||||
'UPDATE backtests SET status = $1, error = $2, updated_at = NOW() WHERE id = $3',
|
||||
[status, error, id]
|
||||
);
|
||||
} else {
|
||||
await this.container.postgres.query(
|
||||
'UPDATE backtests SET status = $1, updated_at = NOW() WHERE id = $2',
|
||||
[status, id]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -154,4 +267,28 @@ export class BacktestService {
|
|||
logger.error('Failed to stop backtest in orchestrator', { backtestId: id, error });
|
||||
}
|
||||
}
|
||||
|
||||
private async saveBacktestResults(backtestId: string, result: any): Promise<void> {
|
||||
try {
|
||||
await this.container.postgres.query(
|
||||
`INSERT INTO backtest_results
|
||||
(backtest_id, completed_at, metrics, equity_curve, ohlc_data, trades, positions, analytics, execution_time)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
||||
[
|
||||
backtestId,
|
||||
result.completedAt || new Date(),
|
||||
JSON.stringify(result.metrics || {}),
|
||||
JSON.stringify(result.equity || result.equityCurve || []),
|
||||
JSON.stringify(result.ohlcData || {}),
|
||||
JSON.stringify(result.trades || []),
|
||||
JSON.stringify(result.positions || result.finalPositions || {}),
|
||||
JSON.stringify(result.analytics || {}),
|
||||
result.executionTime || 0
|
||||
]
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('Failed to save backtest results', { backtestId, error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ import { DashboardPage } from '@/features/dashboard';
|
|||
import { ExchangesPage } from '@/features/exchanges';
|
||||
import { MonitoringPage } from '@/features/monitoring';
|
||||
import { PipelinePage } from '@/features/pipeline';
|
||||
import { BacktestPage } from '@/features/backtest';
|
||||
import { BacktestListPage, BacktestDetailPage } from '@/features/backtest';
|
||||
import { SymbolsPage, SymbolDetailPage } from '@/features/symbols';
|
||||
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
|
|
@ -31,7 +31,10 @@ export function App() {
|
|||
path="analytics"
|
||||
element={<div className="p-4">Analytics Page - Coming Soon</div>}
|
||||
/>
|
||||
<Route path="backtest" element={<BacktestPage />} />
|
||||
<Route path="backtests">
|
||||
<Route index element={<BacktestListPage />} />
|
||||
<Route path=":id" element={<BacktestDetailPage />} />
|
||||
</Route>
|
||||
<Route path="settings" element={<div className="p-4">Settings Page - Coming Soon</div>} />
|
||||
<Route path="system/monitoring" element={<MonitoringPage />} />
|
||||
<Route path="system/pipeline" element={<PipelinePage />} />
|
||||
|
|
|
|||
252
apps/stock/web-app/src/features/backtest/BacktestDetailPage.tsx
Normal file
252
apps/stock/web-app/src/features/backtest/BacktestDetailPage.tsx
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { BacktestConfiguration } from './components/BacktestConfiguration';
|
||||
import { BacktestMetrics } from './components/BacktestMetrics';
|
||||
import { BacktestChart } from './components/BacktestChart';
|
||||
import { BacktestTrades } from './components/BacktestTrades';
|
||||
import { useBacktest } from './hooks/useBacktest';
|
||||
import type { BacktestConfig, BacktestResult as LocalBacktestResult } from './types/backtest.types';
|
||||
import { ArrowLeftIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
const tabs = [
|
||||
{ id: 'settings', name: 'Settings' },
|
||||
{ id: 'metrics', name: 'Performance Metrics' },
|
||||
{ id: 'chart', name: 'Chart' },
|
||||
{ id: 'trades', name: 'Trades' },
|
||||
];
|
||||
|
||||
export function BacktestDetailPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [activeTab, setActiveTab] = useState('settings');
|
||||
const {
|
||||
backtest,
|
||||
results,
|
||||
isLoading,
|
||||
isPolling,
|
||||
error,
|
||||
loadBacktest,
|
||||
createBacktest,
|
||||
updateBacktest,
|
||||
cancelBacktest,
|
||||
} = useBacktest();
|
||||
|
||||
// Local state to bridge between the API format and the existing UI components
|
||||
const [config, setConfig] = useState<BacktestConfig | null>(null);
|
||||
const [adaptedResults, setAdaptedResults] = useState<LocalBacktestResult | null>(null);
|
||||
|
||||
// Load the specific backtest on mount
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
loadBacktest(id);
|
||||
}
|
||||
}, [id, loadBacktest]);
|
||||
|
||||
// Adapt the backtest data to config format when loaded
|
||||
useEffect(() => {
|
||||
if (backtest && !config) {
|
||||
const backtestConfig: BacktestConfig = {
|
||||
name: backtest.config?.name || '',
|
||||
startDate: new Date(backtest.startDate),
|
||||
endDate: new Date(backtest.endDate),
|
||||
initialCapital: backtest.initialCapital,
|
||||
symbols: backtest.symbols,
|
||||
strategy: backtest.strategy,
|
||||
speedMultiplier: backtest.config?.speedMultiplier || 1,
|
||||
commission: backtest.config?.commission || 0.001,
|
||||
slippage: backtest.config?.slippage || 0.0001,
|
||||
};
|
||||
setConfig(backtestConfig);
|
||||
}
|
||||
}, [backtest, config]);
|
||||
|
||||
// Adapt the backtest status from API format to local format
|
||||
const status = backtest ?
|
||||
(backtest.status === 'pending' ? 'configured' :
|
||||
backtest.status === 'running' ? 'running' :
|
||||
backtest.status === 'completed' ? 'completed' :
|
||||
backtest.status === 'failed' ? 'error' :
|
||||
backtest.status === 'cancelled' ? 'stopped' : 'idle') : 'idle';
|
||||
|
||||
// Current time is not available in the new API, so we'll estimate it based on progress
|
||||
const currentTime = null;
|
||||
|
||||
// No adaptation needed - results are already in the correct format
|
||||
useEffect(() => {
|
||||
setAdaptedResults(results);
|
||||
}, [results]);
|
||||
|
||||
const handleRunBacktest = useCallback(async () => {
|
||||
if (!config) return;
|
||||
|
||||
const backtestRequest = {
|
||||
strategy: config.strategy,
|
||||
symbols: config.symbols,
|
||||
startDate: config.startDate.toISOString().split('T')[0],
|
||||
endDate: config.endDate.toISOString().split('T')[0],
|
||||
initialCapital: config.initialCapital,
|
||||
config: {
|
||||
name: config.name,
|
||||
commission: config.commission,
|
||||
slippage: config.slippage,
|
||||
speedMultiplier: config.speedMultiplier,
|
||||
useTypeScriptImplementation: true,
|
||||
},
|
||||
};
|
||||
|
||||
await createBacktest(backtestRequest);
|
||||
}, [config, createBacktest]);
|
||||
|
||||
const handleConfigSubmit = useCallback(async (newConfig: BacktestConfig) => {
|
||||
setConfig(newConfig);
|
||||
setAdaptedResults(null);
|
||||
|
||||
const backtestRequest = {
|
||||
strategy: newConfig.strategy,
|
||||
symbols: newConfig.symbols,
|
||||
startDate: newConfig.startDate.toISOString().split('T')[0],
|
||||
endDate: newConfig.endDate.toISOString().split('T')[0],
|
||||
initialCapital: newConfig.initialCapital,
|
||||
config: {
|
||||
name: newConfig.name,
|
||||
commission: newConfig.commission,
|
||||
slippage: newConfig.slippage,
|
||||
speedMultiplier: newConfig.speedMultiplier,
|
||||
useTypeScriptImplementation: true, // Enable TypeScript strategy execution
|
||||
},
|
||||
};
|
||||
|
||||
// If we have an existing backtest ID, update it, otherwise create new
|
||||
if (id) {
|
||||
await updateBacktest(id, backtestRequest);
|
||||
} else {
|
||||
await createBacktest(backtestRequest);
|
||||
}
|
||||
}, [id, createBacktest, updateBacktest]);
|
||||
|
||||
const handleStop = useCallback(async () => {
|
||||
await cancelBacktest();
|
||||
}, [cancelBacktest]);
|
||||
|
||||
const renderTabContent = () => {
|
||||
switch (activeTab) {
|
||||
case 'settings':
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<BacktestConfiguration
|
||||
onSubmit={handleConfigSubmit}
|
||||
disabled={status === 'running'}
|
||||
initialConfig={config}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case 'metrics':
|
||||
return (
|
||||
<div className="h-full overflow-y-auto">
|
||||
<BacktestMetrics
|
||||
result={adaptedResults}
|
||||
isLoading={isLoading || isPolling}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case 'chart':
|
||||
return (
|
||||
<BacktestChart
|
||||
result={adaptedResults}
|
||||
isLoading={isLoading || isPolling}
|
||||
/>
|
||||
);
|
||||
case 'trades':
|
||||
return (
|
||||
<div className="h-full overflow-y-auto">
|
||||
<BacktestTrades
|
||||
result={adaptedResults}
|
||||
isLoading={isLoading || isPolling}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
{/* Header with Tabs */}
|
||||
<div className="flex-shrink-0 border-b border-border">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
onClick={() => navigate('/backtests')}
|
||||
className="p-2 hover:bg-surface-tertiary transition-colors"
|
||||
aria-label="Back to backtests"
|
||||
>
|
||||
<ArrowLeftIcon className="h-5 w-5 text-text-secondary" />
|
||||
</button>
|
||||
<div className="px-2">
|
||||
<h1 className="text-sm font-bold text-text-primary flex items-center gap-1">
|
||||
{backtest?.config?.name || config?.name || 'Backtest Detail'}
|
||||
{id && (
|
||||
<span className="text-xs font-normal text-text-secondary">
|
||||
ID: {id}
|
||||
</span>
|
||||
)}
|
||||
</h1>
|
||||
<p className="text-xs text-text-secondary">
|
||||
{backtest?.strategy || config?.strategy || 'No strategy selected'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<nav className="flex ml-4" aria-label="Tabs">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`
|
||||
whitespace-nowrap py-2 px-3 border-b-2 font-medium text-sm
|
||||
${activeTab === tab.id
|
||||
? 'border-primary-500 text-primary-500'
|
||||
: 'border-transparent text-text-secondary hover:text-text-primary hover:border-border'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{tab.name}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Run/Stop button */}
|
||||
<div className="pr-4">
|
||||
{status === 'running' ? (
|
||||
<button
|
||||
onClick={handleStop}
|
||||
className="px-4 py-1.5 bg-error/10 text-error rounded-md text-sm font-medium hover:bg-error/20 transition-colors"
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
) : status !== 'running' && config && config.symbols.length > 0 ? (
|
||||
<button
|
||||
onClick={handleRunBacktest}
|
||||
className="px-4 py-1.5 bg-primary-500 text-white rounded-md text-sm font-medium hover:bg-primary-600 transition-colors"
|
||||
>
|
||||
Run Backtest
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="flex-1 overflow-auto p-4">
|
||||
{error && (
|
||||
<div className="mb-4 p-4 bg-error/10 border border-error/20 rounded-lg">
|
||||
<p className="text-sm text-error">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
{renderTabContent()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,14 +1,56 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useBacktestList } from './hooks/useBacktest';
|
||||
import { Link } from 'react-router-dom';
|
||||
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':
|
||||
|
|
@ -37,12 +79,13 @@ export function BacktestListPage() {
|
|||
View and manage your backtest runs
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
to="/backtests/new"
|
||||
className="px-4 py-2 bg-primary-500 text-white rounded-md text-sm font-medium hover:bg-primary-600 transition-colors"
|
||||
<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"
|
||||
>
|
||||
New Backtest
|
||||
</Link>
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
<span>New Backtest</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
|
|
@ -58,12 +101,13 @@ export function BacktestListPage() {
|
|||
) : 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>
|
||||
<Link
|
||||
to="/backtests/new"
|
||||
className="inline-flex px-4 py-2 bg-primary-500 text-white rounded-md text-sm font-medium hover:bg-primary-600 transition-colors"
|
||||
<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"
|
||||
>
|
||||
Create Your First Backtest
|
||||
</Link>
|
||||
<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">
|
||||
|
|
@ -94,8 +138,13 @@ export function BacktestListPage() {
|
|||
<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 text-sm font-medium capitalize ${getStatusColor(backtest.status)}`}>
|
||||
{backtest.status}
|
||||
<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)}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ export function BacktestPage() {
|
|||
endDate: newConfig.endDate.toISOString().split('T')[0],
|
||||
initialCapital: newConfig.initialCapital,
|
||||
config: {
|
||||
name: newConfig.name,
|
||||
commission: newConfig.commission,
|
||||
slippage: newConfig.slippage,
|
||||
speedMultiplier: newConfig.speedMultiplier,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
import type { BacktestResult } from '../types/backtest.types';
|
||||
import { useState, useMemo } from 'react';
|
||||
import { Chart } from '../../../components/charts';
|
||||
|
||||
interface BacktestChartProps {
|
||||
result: BacktestResult | null;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export function BacktestChart({ result, isLoading }: BacktestChartProps) {
|
||||
const [selectedSymbol, setSelectedSymbol] = useState<string>('');
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
if (!result?.equity || !result?.ohlcData) return null;
|
||||
|
||||
const symbols = Object.keys(result.ohlcData);
|
||||
const symbol = selectedSymbol || symbols[0] || '';
|
||||
|
||||
const ohlcData = result.ohlcData[symbol] || [];
|
||||
const equityData = result.equity.map(e => ({
|
||||
time: new Date(e.date).getTime() / 1000,
|
||||
value: e.value
|
||||
}));
|
||||
|
||||
// Find trades for this symbol
|
||||
const symbolTrades = result.trades?.filter(t => t.symbol === symbol) || [];
|
||||
const tradeMarkers = symbolTrades.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)}`
|
||||
}));
|
||||
|
||||
return {
|
||||
ohlcData: ohlcData.map(d => ({
|
||||
time: d.time / 1000,
|
||||
open: d.open,
|
||||
high: d.high,
|
||||
low: d.low,
|
||||
close: d.close,
|
||||
volume: d.volume
|
||||
})),
|
||||
equityData,
|
||||
tradeMarkers,
|
||||
symbols
|
||||
};
|
||||
}, [result, selectedSymbol]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="bg-surface-secondary rounded-lg border border-border p-4 h-full animate-pulse">
|
||||
<div className="h-full bg-surface rounded"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!result || !chartData) {
|
||||
return (
|
||||
<div className="bg-surface-secondary rounded-lg border border-border p-8 text-center h-full flex items-center justify-center">
|
||||
<p className="text-text-secondary">Run a backtest to see the equity curve and price chart.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
{chartData.symbols.length > 1 && (
|
||||
<div className="flex space-x-2 mb-4">
|
||||
<label className="text-sm text-text-secondary">Symbol:</label>
|
||||
<select
|
||||
value={selectedSymbol || chartData.symbols[0]}
|
||||
onChange={(e) => setSelectedSymbol(e.target.value)}
|
||||
className="px-2 py-1 bg-background border border-border rounded text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||
>
|
||||
{chartData.symbols.map(symbol => (
|
||||
<option key={symbol} value={symbol}>{symbol}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1">
|
||||
<Chart
|
||||
data={chartData.ohlcData}
|
||||
equityData={chartData.equityData}
|
||||
markers={chartData.tradeMarkers}
|
||||
height={500}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,13 +1,14 @@
|
|||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import type { BacktestConfig } from '../types';
|
||||
|
||||
interface BacktestConfigurationProps {
|
||||
onSubmit: (config: BacktestConfig) => void;
|
||||
disabled?: boolean;
|
||||
initialConfig?: BacktestConfig;
|
||||
}
|
||||
|
||||
export function BacktestConfiguration({ onSubmit, disabled }: BacktestConfigurationProps) {
|
||||
const [formData, setFormData] = useState<BacktestConfig>({
|
||||
export function BacktestConfiguration({ onSubmit, disabled, initialConfig }: BacktestConfigurationProps) {
|
||||
const [formData, setFormData] = useState<BacktestConfig>(initialConfig || {
|
||||
name: '',
|
||||
startDate: new Date(new Date().setMonth(new Date().getMonth() - 6)), // 6 months ago
|
||||
endDate: new Date(),
|
||||
|
|
@ -21,6 +22,12 @@ export function BacktestConfiguration({ onSubmit, disabled }: BacktestConfigurat
|
|||
|
||||
const [symbolInput, setSymbolInput] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (initialConfig) {
|
||||
setFormData(initialConfig);
|
||||
}
|
||||
}, [initialConfig]);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (formData.symbols.length === 0) {
|
||||
|
|
@ -62,10 +69,7 @@ export function BacktestConfiguration({ onSubmit, disabled }: BacktestConfigurat
|
|||
};
|
||||
|
||||
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">
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-1">
|
||||
Backtest Name
|
||||
|
|
@ -257,9 +261,8 @@ export function BacktestConfiguration({ onSubmit, disabled }: BacktestConfigurat
|
|||
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
|
||||
Save
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
import type { BacktestResult } from '../types/backtest.types';
|
||||
import { MetricsCard } from './MetricsCard';
|
||||
|
||||
interface BacktestMetricsProps {
|
||||
result: BacktestResult | null;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export function BacktestMetrics({ result, isLoading }: BacktestMetricsProps) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{[...Array(6)].map((_, i) => (
|
||||
<div key={i} className="bg-surface-secondary rounded-lg border border-border p-4 h-24 animate-pulse">
|
||||
<div className="h-4 bg-surface rounded w-1/2 mb-2"></div>
|
||||
<div className="h-6 bg-surface rounded w-3/4"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
return (
|
||||
<div className="bg-surface-secondary rounded-lg border border-border p-8 text-center">
|
||||
<p className="text-text-secondary">Run a backtest to see performance metrics.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { metrics } = result;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<MetricsCard
|
||||
title="Total Return"
|
||||
value={`${(metrics.totalReturn * 100).toFixed(2)}%`}
|
||||
color={metrics.totalReturn >= 0 ? 'success' : 'error'}
|
||||
/>
|
||||
<MetricsCard
|
||||
title="Sharpe Ratio"
|
||||
value={metrics.sharpeRatio.toFixed(2)}
|
||||
color={metrics.sharpeRatio > 1 ? 'success' : metrics.sharpeRatio > 0 ? 'warning' : 'error'}
|
||||
/>
|
||||
<MetricsCard
|
||||
title="Max Drawdown"
|
||||
value={`${(metrics.maxDrawdown * 100).toFixed(2)}%`}
|
||||
color={metrics.maxDrawdown > -0.2 ? 'warning' : 'error'}
|
||||
/>
|
||||
<MetricsCard
|
||||
title="Win Rate"
|
||||
value={`${(metrics.winRate * 100).toFixed(1)}%`}
|
||||
color={metrics.winRate > 0.5 ? 'success' : 'warning'}
|
||||
/>
|
||||
<MetricsCard
|
||||
title="Total Trades"
|
||||
value={metrics.totalTrades.toString()}
|
||||
/>
|
||||
<MetricsCard
|
||||
title="Profit Factor"
|
||||
value={metrics.profitFactor.toFixed(2)}
|
||||
color={metrics.profitFactor > 1.5 ? 'success' : metrics.profitFactor > 1 ? 'warning' : 'error'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="bg-surface-secondary rounded-lg border border-border p-4">
|
||||
<h4 className="text-sm font-medium text-text-secondary mb-3">Trade Statistics</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary text-sm">Profitable Trades</span>
|
||||
<span className="text-text-primary text-sm font-medium">{metrics.profitableTrades}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary text-sm">Average Win</span>
|
||||
<span className="text-success text-sm font-medium">${metrics.avgWin.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary text-sm">Average Loss</span>
|
||||
<span className="text-error text-sm font-medium">${Math.abs(metrics.avgLoss).toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary text-sm">Expectancy</span>
|
||||
<span className={`text-sm font-medium ${metrics.expectancy >= 0 ? 'text-success' : 'text-error'}`}>
|
||||
${metrics.expectancy.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-surface-secondary rounded-lg border border-border p-4">
|
||||
<h4 className="text-sm font-medium text-text-secondary mb-3">Risk Metrics</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary text-sm">Calmar Ratio</span>
|
||||
<span className="text-text-primary text-sm font-medium">{metrics.calmarRatio.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary text-sm">Sortino Ratio</span>
|
||||
<span className="text-text-primary text-sm font-medium">{metrics.sortinoRatio.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary text-sm">Exposure Time</span>
|
||||
<span className="text-text-primary text-sm font-medium">
|
||||
{result.analytics?.exposureTime ? `${(result.analytics.exposureTime * 100).toFixed(1)}%` : 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
import type { BacktestResult } from '../types/backtest.types';
|
||||
import { TradeLog } from './TradeLog';
|
||||
|
||||
interface BacktestTradesProps {
|
||||
result: BacktestResult | null;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export function BacktestTrades({ result, isLoading }: BacktestTradesProps) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="bg-surface-secondary rounded-lg border border-border p-4 animate-pulse">
|
||||
<div className="space-y-3">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<div key={i} className="h-16 bg-surface rounded"></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!result || !result.trades || result.trades.length === 0) {
|
||||
return (
|
||||
<div className="bg-surface-secondary rounded-lg border border-border p-8 text-center">
|
||||
<p className="text-text-secondary">No trades executed in this backtest.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<div className="overflow-x-auto bg-surface-secondary rounded-lg border border-border">
|
||||
<table className="w-full">
|
||||
<thead className="bg-background border-b border-border">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase tracking-wider">
|
||||
Symbol
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase tracking-wider">
|
||||
Side
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase tracking-wider">
|
||||
Entry Date
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase tracking-wider">
|
||||
Entry Price
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase tracking-wider">
|
||||
Exit Date
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase tracking-wider">
|
||||
Exit Price
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase tracking-wider">
|
||||
Quantity
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase tracking-wider">
|
||||
P&L
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase tracking-wider">
|
||||
P&L %
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{result.trades.map((trade) => (
|
||||
<tr key={trade.id} className="hover:bg-background/50 transition-colors">
|
||||
<td className="px-4 py-3 text-sm text-text-primary font-medium">
|
||||
{trade.symbol}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm">
|
||||
<span className={`inline-flex px-2 py-1 text-xs font-medium rounded-md ${
|
||||
trade.side === 'buy'
|
||||
? 'bg-success/10 text-success'
|
||||
: 'bg-error/10 text-error'
|
||||
}`}>
|
||||
{trade.side.toUpperCase()}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-text-secondary">
|
||||
{new Date(trade.entryDate).toLocaleString()}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-text-primary">
|
||||
${trade.entryPrice.toFixed(2)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-text-secondary">
|
||||
{trade.exitDate ? new Date(trade.exitDate).toLocaleString() : '-'}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-text-primary">
|
||||
${trade.exitPrice.toFixed(2)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-text-primary">
|
||||
{trade.quantity}
|
||||
</td>
|
||||
<td className={`px-4 py-3 text-sm font-medium ${
|
||||
trade.pnl >= 0 ? 'text-success' : 'text-error'
|
||||
}`}>
|
||||
${trade.pnl.toFixed(2)}
|
||||
</td>
|
||||
<td className={`px-4 py-3 text-sm font-medium ${
|
||||
trade.pnlPercent >= 0 ? 'text-success' : 'text-error'
|
||||
}`}>
|
||||
{trade.pnlPercent.toFixed(2)}%
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{result.positions && Object.keys(result.positions).length > 0 && (
|
||||
<div className="mt-4 bg-surface-secondary rounded-lg border border-border p-4">
|
||||
<h4 className="text-sm font-medium text-text-secondary mb-3">Open Positions</h4>
|
||||
<div className="space-y-2">
|
||||
{Object.entries(result.positions).map(([symbol, position]) => (
|
||||
<div key={symbol} className="flex justify-between items-center p-2 bg-background rounded">
|
||||
<span className="text-sm font-medium text-text-primary">{symbol}</span>
|
||||
<div className="flex space-x-4 text-sm">
|
||||
<span className="text-text-secondary">
|
||||
Qty: {position.quantity}
|
||||
</span>
|
||||
<span className="text-text-secondary">
|
||||
Avg: ${position.averagePrice.toFixed(2)}
|
||||
</span>
|
||||
<span className={position.unrealizedPnl >= 0 ? 'text-success' : 'text-error'}>
|
||||
P&L: ${position.unrealizedPnl.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -11,7 +11,9 @@ interface UseBacktestReturn {
|
|||
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;
|
||||
}
|
||||
|
|
@ -61,6 +63,35 @@ export function useBacktest(): UseBacktestReturn {
|
|||
}
|
||||
}, []);
|
||||
|
||||
// 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);
|
||||
|
|
@ -75,7 +106,7 @@ export function useBacktest(): UseBacktestReturn {
|
|||
setIsPolling(true);
|
||||
pollingIntervalRef.current = setInterval(() => {
|
||||
pollStatus(newBacktest.id);
|
||||
}, 200); // Poll every 2 seconds
|
||||
}, 2000); // Poll every 2 seconds
|
||||
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to create backtest');
|
||||
|
|
@ -84,6 +115,29 @@ export function useBacktest(): UseBacktestReturn {
|
|||
}
|
||||
}, [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;
|
||||
|
|
@ -133,7 +187,9 @@ export function useBacktest(): UseBacktestReturn {
|
|||
isLoading,
|
||||
isPolling,
|
||||
error,
|
||||
loadBacktest,
|
||||
createBacktest,
|
||||
updateBacktest,
|
||||
cancelBacktest,
|
||||
reset,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,2 +1,4 @@
|
|||
export { BacktestPage } from './BacktestPage';
|
||||
export { BacktestListPage } from './BacktestListPage';
|
||||
export { BacktestDetailPage } from './BacktestDetailPage';
|
||||
export * from './types';
|
||||
|
|
@ -32,7 +32,7 @@ export const navigation: NavigationItem[] = [
|
|||
{ name: 'Portfolio', href: '/portfolio', icon: ChartBarIcon },
|
||||
{ name: 'Strategies', href: '/strategies', icon: DocumentTextIcon },
|
||||
{ name: 'Analytics', href: '/analytics', icon: PresentationChartLineIcon },
|
||||
{ name: 'Backtest', href: '/backtest', icon: BeakerIcon },
|
||||
{ name: 'Backtests', href: '/backtests', icon: BeakerIcon },
|
||||
{
|
||||
name: 'System',
|
||||
icon: ServerStackIcon,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue