import { v4 as uuidv4 } from 'uuid'; import { getLogger } from '@stock-bot/logger'; const logger = getLogger('backtest-service'); // Use environment variable or default const ORCHESTRATOR_URL = process.env.ORCHESTRATOR_URL || 'http://localhost:2004'; export interface BacktestRequest { strategy: string; symbols: string[]; startDate: string; endDate: string; initialCapital: number; config?: Record; } export interface BacktestJob { id: string; status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'; strategy: string; symbols: string[]; startDate: Date; endDate: Date; initialCapital: number; config: Record; createdAt: Date; updatedAt: Date; error?: string; } // In-memory storage for demo (replace with database) const backtestStore = new Map(); const backtestResults = new Map(); export class BacktestService { 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(), }; backtestStore.set(backtestId, backtest); // Call orchestrator to run backtest try { const response = await fetch(`${ORCHESTRATOR_URL}/api/backtest/run`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ mode: 'backtest', startDate: new Date(request.startDate).toISOString(), endDate: new Date(request.endDate).toISOString(), symbols: request.symbols, initialCapital: request.initialCapital, dataFrequency: '1d', // Default to daily speed: 'max', // Default speed fillModel: { slippage: 'realistic', marketImpact: true, partialFills: true } }), }); if (!response.ok) { throw new Error(`Orchestrator returned ${response.status}`); } const result = await response.json(); // Store result when available if (result.performance) { // Backtest completed immediately backtest.status = 'completed'; backtestStore.set(backtestId, backtest); backtestResults.set(backtestId, result); } else { // Update status to running if not completed backtest.status = 'running'; backtestStore.set(backtestId, backtest); } logger.info('Backtest started in orchestrator', { backtestId, result }); } catch (error) { logger.error('Failed to start backtest in orchestrator', { backtestId, error }); backtest.status = 'failed'; backtest.error = error.message; } return backtest; } async getBacktest(id: string): Promise { return backtestStore.get(id) || null; } async getBacktestResults(id: string): Promise { const results = backtestResults.get(id); if (!results) return null; // Transform orchestrator response to frontend expected format return { backtestId: results.id || id, metrics: { totalReturn: results.performance?.totalReturn || 0, sharpeRatio: results.performance?.sharpeRatio || 0, maxDrawdown: results.performance?.maxDrawdown || 0, winRate: results.performance?.winRate || 0, totalTrades: results.performance?.totalTrades || 0, profitFactor: results.performance?.profitFactor }, equity: results.equityCurve?.map((point: any) => ({ date: new Date(point.timestamp).toISOString(), value: point.value })) || [], trades: results.trades?.map((trade: any) => ({ symbol: trade.symbol, entryDate: new Date(trade.entryTime).toISOString(), exitDate: new Date(trade.exitTime).toISOString(), entryPrice: trade.entryPrice, exitPrice: trade.exitPrice, quantity: trade.quantity, pnl: trade.pnl })) || [], ohlcData: results.ohlcData || {} }; } 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); } 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); } } async cancelBacktest(id: string): Promise { await this.updateBacktestStatus(id, 'cancelled'); // Call orchestrator to stop backtest try { await fetch(`${ORCHESTRATOR_URL}/api/backtest/stop`, { method: 'POST', }); } catch (error) { logger.error('Failed to stop backtest in orchestrator', { backtestId: id, error }); } } }