stock-bot/apps/stock/web-api/src/services/backtest.service.ts

174 lines
No EOL
5.5 KiB
TypeScript

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<string, any>;
}
export interface BacktestJob {
id: string;
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
strategy: string;
symbols: string[];
startDate: Date;
endDate: Date;
initialCapital: number;
config: Record<string, any>;
createdAt: Date;
updatedAt: Date;
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 {
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(),
};
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<BacktestJob | null> {
return backtestStore.get(id) || null;
}
async getBacktestResults(id: string): Promise<any> {
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<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);
}
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);
}
}
async cancelBacktest(id: string): Promise<void> {
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 });
}
}
}