174 lines
No EOL
5.5 KiB
TypeScript
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 });
|
|
}
|
|
}
|
|
} |