diff --git a/apps/stock/engine/index.node b/apps/stock/engine/index.node
index 55c1023..976e7bc 100755
Binary files a/apps/stock/engine/index.node and b/apps/stock/engine/index.node differ
diff --git a/apps/stock/web-api/src/index.ts b/apps/stock/web-api/src/index.ts
index 4e2461a..baa3029 100644
--- a/apps/stock/web-api/src/index.ts
+++ b/apps/stock/web-api/src/index.ts
@@ -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;
}
diff --git a/apps/stock/web-api/src/migrations/migration-runner.ts b/apps/stock/web-api/src/migrations/migration-runner.ts
new file mode 100644
index 0000000..3271dde
--- /dev/null
+++ b/apps/stock/web-api/src/migrations/migration-runner.ts
@@ -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 {
+ 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;
+ }
+}
\ No newline at end of file
diff --git a/apps/stock/web-api/src/routes/backtest.routes.ts b/apps/stock/web-api/src/routes/backtest.routes.ts
index 16b077e..3bd35aa 100644
--- a/apps/stock/web-api/src/routes/backtest.routes.ts
+++ b/apps/stock/web-api/src/routes/backtest.routes.ts
@@ -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) => {
diff --git a/apps/stock/web-api/src/services/backtest.service.ts b/apps/stock/web-api/src/services/backtest.service.ts
index e95fddd..3f90b88 100644
--- a/apps/stock/web-api/src/services/backtest.service.ts
+++ b/apps/stock/web-api/src/services/backtest.service.ts
@@ -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();
-const backtestResults = new Map();
-
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 {
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 {
- 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 {
- // 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 {
- 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 {
- 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 {
+ 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;
+ }
+ }
}
\ No newline at end of file
diff --git a/apps/stock/web-app/src/app/App.tsx b/apps/stock/web-app/src/app/App.tsx
index e95f970..553be54 100644
--- a/apps/stock/web-app/src/app/App.tsx
+++ b/apps/stock/web-app/src/app/App.tsx
@@ -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={Analytics Page - Coming Soon
}
/>
- } />
+
+ } />
+ } />
+
Settings Page - Coming Soon} />
} />
} />
diff --git a/apps/stock/web-app/src/features/backtest/BacktestDetailPage.tsx b/apps/stock/web-app/src/features/backtest/BacktestDetailPage.tsx
new file mode 100644
index 0000000..6ba6a59
--- /dev/null
+++ b/apps/stock/web-app/src/features/backtest/BacktestDetailPage.tsx
@@ -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(null);
+ const [adaptedResults, setAdaptedResults] = useState(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 (
+
+
+
+ );
+ case 'metrics':
+ return (
+
+
+
+ );
+ case 'chart':
+ return (
+
+ );
+ case 'trades':
+ return (
+
+
+
+ );
+ default:
+ return null;
+ }
+ };
+
+ return (
+
+ {/* Header with Tabs */}
+
+
+
+
+
+
+ {backtest?.config?.name || config?.name || 'Backtest Detail'}
+ {id && (
+
+ ID: {id}
+
+ )}
+
+
+ {backtest?.strategy || config?.strategy || 'No strategy selected'}
+
+
+
+ {/* Tabs */}
+
+
+
+ {/* Run/Stop button */}
+
+ {status === 'running' ? (
+
+ ) : status !== 'running' && config && config.symbols.length > 0 ? (
+
+ ) : null}
+
+
+
+
+ {/* Tab Content */}
+
+ {error && (
+
+ )}
+ {renderTabContent()}
+
+
+ );
+}
\ No newline at end of file
diff --git a/apps/stock/web-app/src/features/backtest/BacktestListPage.tsx b/apps/stock/web-app/src/features/backtest/BacktestListPage.tsx
index 5a8a4e1..8c1be67 100644
--- a/apps/stock/web-app/src/features/backtest/BacktestListPage.tsx
+++ b/apps/stock/web-app/src/features/backtest/BacktestListPage.tsx
@@ -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(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 ;
+ case 'running':
+ return ;
+ case 'pending':
+ return ;
+ case 'failed':
+ return ;
+ case 'cancelled':
+ return ;
+ default:
+ return ;
+ }
+ };
+
const getStatusColor = (status: string) => {
switch (status) {
case 'completed':
@@ -37,12 +79,13 @@ export function BacktestListPage() {
View and manage your backtest runs
- 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
-
+
+ New Backtest
+
{error && (
@@ -58,12 +101,13 @@ export function BacktestListPage() {
) : backtests.length === 0 ? (
No backtests found
-
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
-
+
+
Create Your First Backtest
+
) : (
@@ -94,8 +138,13 @@ export function BacktestListPage() {
{new Date(backtest.startDate).toLocaleDateString()} - {new Date(backtest.endDate).toLocaleDateString()}
|
-
- {backtest.status}
+ |
+
+ {getStatusIcon(backtest.status)}
+
+ {backtest.status}
+
+
|
{formatDate(backtest.createdAt)}
diff --git a/apps/stock/web-app/src/features/backtest/BacktestPage.tsx b/apps/stock/web-app/src/features/backtest/BacktestPage.tsx
index 092393c..3e9ade3 100644
--- a/apps/stock/web-app/src/features/backtest/BacktestPage.tsx
+++ b/apps/stock/web-app/src/features/backtest/BacktestPage.tsx
@@ -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,
diff --git a/apps/stock/web-app/src/features/backtest/components/BacktestChart.tsx b/apps/stock/web-app/src/features/backtest/components/BacktestChart.tsx
new file mode 100644
index 0000000..87036f1
--- /dev/null
+++ b/apps/stock/web-app/src/features/backtest/components/BacktestChart.tsx
@@ -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('');
+
+ 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 (
+
+ );
+ }
+
+ if (!result || !chartData) {
+ return (
+
+ Run a backtest to see the equity curve and price chart.
+
+ );
+ }
+
+ return (
+
+ {chartData.symbols.length > 1 && (
+
+
+
+
+ )}
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/apps/stock/web-app/src/features/backtest/components/BacktestConfiguration.tsx b/apps/stock/web-app/src/features/backtest/components/BacktestConfiguration.tsx
index 70cf6f2..e63ec65 100644
--- a/apps/stock/web-app/src/features/backtest/components/BacktestConfiguration.tsx
+++ b/apps/stock/web-app/src/features/backtest/components/BacktestConfiguration.tsx
@@ -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({
+export function BacktestConfiguration({ onSubmit, disabled, initialConfig }: BacktestConfigurationProps) {
+ const [formData, setFormData] = useState(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 (
- |