initial backtests
This commit is contained in:
parent
fa70ada2bb
commit
5a3a23a2ba
6 changed files with 400 additions and 129 deletions
|
|
@ -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>();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue