initial backtests

This commit is contained in:
Boki 2025-07-03 09:07:45 -04:00
parent fa70ada2bb
commit 5a3a23a2ba
6 changed files with 400 additions and 129 deletions

View file

@ -14,14 +14,78 @@ interface BacktestEvent {
}
interface BacktestResult {
id: string;
config: any;
performance: PerformanceMetrics;
trades: any[];
equityCurve: { timestamp: number; value: number }[];
drawdown: { timestamp: number; value: number }[];
dailyReturns: number[];
finalPositions: any[];
// Identification
backtestId: string;
status: 'completed' | 'failed' | 'cancelled';
completedAt: string;
// Configuration
config: {
name: string;
strategy: string;
symbols: string[];
startDate: string;
endDate: string;
initialCapital: number;
commission: number;
slippage: number;
dataFrequency: string;
};
// Performance metrics
metrics: {
totalReturn: number;
sharpeRatio: number;
maxDrawdown: number;
winRate: number;
totalTrades: number;
profitFactor: number;
profitableTrades: number;
avgWin: number;
avgLoss: number;
expectancy: number;
calmarRatio: number;
sortinoRatio: number;
};
// Chart data
equity: Array<{ date: string; value: number }>;
ohlcData: Record<string, any[]>;
// Trade history
trades: Array<{
id: string;
symbol: string;
entryDate: string;
exitDate: string | null;
entryPrice: number;
exitPrice: number;
quantity: number;
side: string;
pnl: number;
pnlPercent: number;
commission: number;
duration: number;
}>;
// Positions
positions: Array<{
symbol: string;
quantity: number;
averagePrice: number;
currentPrice: number;
unrealizedPnl: number;
realizedPnl: number;
}>;
// Analytics
analytics: {
drawdownSeries: { timestamp: number; value: number }[];
dailyReturns: number[];
monthlyReturns: Record<string, number>;
exposureTime: number;
riskMetrics: Record<string, number>;
};
}
export class BacktestEngine extends EventEmitter {
@ -117,17 +181,85 @@ export class BacktestEngine extends EventEmitter {
// Get final positions
const finalPositions = await this.getFinalPositions();
// Store results
// Create comprehensive frontend-ready result
const result: BacktestResult = {
id: backtestId,
config: validatedConfig,
performance,
trades: this.trades,
equityCurve: this.equityCurve,
drawdown: this.calculateDrawdown(),
dailyReturns: this.calculateDailyReturns(),
finalPositions,
ohlcData: this.getOHLCData(marketData, validatedConfig.symbols)
// Identification
backtestId,
status: 'completed' as const,
completedAt: new Date().toISOString(),
// Configuration used
config: {
name: validatedConfig.name || 'Backtest',
strategy: validatedConfig.strategy,
symbols: validatedConfig.symbols,
startDate: validatedConfig.startDate,
endDate: validatedConfig.endDate,
initialCapital: validatedConfig.initialCapital,
commission: validatedConfig.commission || 0,
slippage: validatedConfig.slippage || 0,
dataFrequency: validatedConfig.dataFrequency || '1d'
},
// Performance metrics (frontend-ready)
metrics: {
totalReturn: performance.totalReturn,
sharpeRatio: performance.sharpeRatio,
maxDrawdown: performance.maxDrawdown,
winRate: performance.winRate,
totalTrades: performance.totalTrades,
profitFactor: performance.profitFactor || 0,
profitableTrades: Math.round(performance.totalTrades * performance.winRate / 100),
avgWin: performance.avgWin || 0,
avgLoss: performance.avgLoss || 0,
expectancy: performance.expectancy || 0,
calmarRatio: performance.calmarRatio || 0,
sortinoRatio: performance.sortinoRatio || 0
},
// Chart data (frontend-ready format)
equity: this.equityCurve.map(point => ({
date: new Date(point.timestamp).toISOString(),
value: point.value
})),
// OHLC data for charts
ohlcData: this.getOHLCData(marketData, validatedConfig.symbols),
// Trade history (frontend-ready)
trades: this.trades.map(trade => ({
id: `${trade.symbol}-${trade.entryTime}`,
symbol: trade.symbol,
entryDate: new Date(trade.entryTime).toISOString(),
exitDate: trade.exitTime ? new Date(trade.exitTime).toISOString() : null,
entryPrice: trade.entryPrice,
exitPrice: trade.exitPrice || trade.currentPrice,
quantity: trade.quantity,
side: trade.side,
pnl: trade.pnl || 0,
pnlPercent: trade.returnPct || 0,
commission: trade.commission || 0,
duration: trade.holdingPeriod || 0
})),
// Final positions
positions: finalPositions.map(pos => ({
symbol: pos.symbol,
quantity: pos.quantity,
averagePrice: pos.avgPrice,
currentPrice: pos.currentPrice || pos.avgPrice,
unrealizedPnl: pos.unrealizedPnl || 0,
realizedPnl: pos.realizedPnl || 0
})),
// Additional analytics
analytics: {
drawdownSeries: this.calculateDrawdown(),
dailyReturns: this.calculateDailyReturns(),
monthlyReturns: this.calculateMonthlyReturns(),
exposureTime: this.calculateExposureTime(),
riskMetrics: this.calculateRiskMetrics()
}
};
await this.storeResults(result);
@ -484,6 +616,99 @@ export class BacktestEngine extends EventEmitter {
return drawdowns;
}
private calculateMonthlyReturns(): Record<string, number> {
const monthlyReturns: Record<string, number> = {};
const monthlyEquity = new Map<string, { start: number; end: number }>();
for (const point of this.equityCurve) {
const date = new Date(point.timestamp);
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
if (!monthlyEquity.has(monthKey)) {
monthlyEquity.set(monthKey, { start: point.value, end: point.value });
} else {
const month = monthlyEquity.get(monthKey)!;
month.end = point.value;
}
}
for (const [month, values] of monthlyEquity) {
monthlyReturns[month] = ((values.end - values.start) / values.start) * 100;
}
return monthlyReturns;
}
private calculateExposureTime(): number {
if (this.trades.length === 0) return 0;
let totalExposureTime = 0;
for (const trade of this.trades) {
if (trade.exitTime) {
totalExposureTime += trade.exitTime - trade.entryTime;
}
}
// Use equity curve to determine actual trading period
const startTime = this.equityCurve.length > 0 ? this.equityCurve[0].timestamp : 0;
const endTime = this.equityCurve.length > 0 ? this.equityCurve[this.equityCurve.length - 1].timestamp : 0;
const totalTime = endTime - startTime;
return totalTime > 0 ? (totalExposureTime / totalTime) * 100 : 0;
}
private calculateRiskMetrics(): Record<string, number> {
const returns = this.calculateDailyReturns();
// Calculate various risk metrics
const avgReturn = returns.reduce((a, b) => a + b, 0) / returns.length;
const variance = returns.reduce((sum, r) => sum + Math.pow(r - avgReturn, 2), 0) / returns.length;
const stdDev = Math.sqrt(variance);
// Downside deviation (for Sortino)
const downsideReturns = returns.filter(r => r < 0);
const downsideVariance = downsideReturns.reduce((sum, r) => sum + Math.pow(r, 2), 0) / downsideReturns.length;
const downsideDeviation = Math.sqrt(downsideVariance);
return {
volatility: stdDev * Math.sqrt(252), // Annualized
downsideDeviation: downsideDeviation * Math.sqrt(252),
var95: this.calculateVaR(returns, 0.95),
var99: this.calculateVaR(returns, 0.99),
cvar95: this.calculateCVaR(returns, 0.95),
maxConsecutiveLosses: this.calculateMaxConsecutiveLosses()
};
}
private calculateVaR(returns: number[], confidence: number): number {
const sorted = [...returns].sort((a, b) => a - b);
const index = Math.floor((1 - confidence) * sorted.length);
return sorted[index] || 0;
}
private calculateCVaR(returns: number[], confidence: number): number {
const sorted = [...returns].sort((a, b) => a - b);
const index = Math.floor((1 - confidence) * sorted.length);
const tail = sorted.slice(0, index + 1);
return tail.reduce((a, b) => a + b, 0) / tail.length;
}
private calculateMaxConsecutiveLosses(): number {
let maxLosses = 0;
let currentLosses = 0;
for (const trade of this.trades) {
if (trade.pnl && trade.pnl < 0) {
currentLosses++;
maxLosses = Math.max(maxLosses, currentLosses);
} else {
currentLosses = 0;
}
}
return maxLosses;
}
private calculateDailyReturns(): number[] {
const dailyReturns: number[] = [];
const dailyEquity = new Map<string, number>();