cleanup and fixed initial capital / commision / splippage
This commit is contained in:
parent
70da2c68e5
commit
d14380d740
19 changed files with 1344 additions and 33 deletions
|
|
@ -101,7 +101,7 @@ export class PerformanceAnalyzer {
|
|||
|
||||
addEquityPoint(date: Date, value: number): void {
|
||||
this.equityCurve.push({ date, value });
|
||||
this.calculateDailyReturns();
|
||||
// Don't calculate daily returns on every point - wait until analyze() is called
|
||||
}
|
||||
|
||||
addTrade(trade: Trade): void {
|
||||
|
|
@ -117,6 +117,9 @@ export class PerformanceAnalyzer {
|
|||
return this.getEmptyMetrics();
|
||||
}
|
||||
|
||||
// Calculate daily returns before analysis
|
||||
this.calculateDailyReturns();
|
||||
|
||||
// Calculate returns
|
||||
const totalReturn = this.calculateTotalReturn();
|
||||
const annualizedReturn = this.calculateAnnualizedReturn();
|
||||
|
|
@ -131,7 +134,7 @@ export class PerformanceAnalyzer {
|
|||
// Risk-adjusted returns
|
||||
const sharpeRatio = this.calculateSharpeRatio(annualizedReturn, volatility);
|
||||
const sortinoRatio = this.calculateSortinoRatio(annualizedReturn, downVolatility);
|
||||
const calmarRatio = annualizedReturn / Math.abs(drawdownAnalysis.maxDrawdown);
|
||||
const calmarRatio = drawdownAnalysis.maxDrawdown > 0 ? annualizedReturn / (drawdownAnalysis.maxDrawdown * 100) : 0;
|
||||
const informationRatio = this.calculateInformationRatio();
|
||||
|
||||
// Trade statistics
|
||||
|
|
@ -148,8 +151,8 @@ export class PerformanceAnalyzer {
|
|||
totalReturn,
|
||||
annualizedReturn,
|
||||
cagr,
|
||||
volatility,
|
||||
downVolatility,
|
||||
volatility: volatility * 100, // Convert to percentage for display
|
||||
downVolatility: downVolatility * 100, // Convert to percentage for display
|
||||
maxDrawdown: drawdownAnalysis.maxDrawdown,
|
||||
maxDrawdownDuration: drawdownAnalysis.maxDrawdownDuration,
|
||||
var95,
|
||||
|
|
@ -289,10 +292,26 @@ export class PerformanceAnalyzer {
|
|||
|
||||
private calculateDailyReturns(): void {
|
||||
this.dailyReturns = [];
|
||||
for (let i = 1; i < this.equityCurve.length; i++) {
|
||||
const prevValue = this.equityCurve[i - 1].value;
|
||||
const currValue = this.equityCurve[i].value;
|
||||
this.dailyReturns.push((currValue - prevValue) / prevValue);
|
||||
|
||||
// First, aggregate equity curve by day (taking last value of each day)
|
||||
const dailyEquity = new Map<string, { date: Date; value: number }>();
|
||||
|
||||
for (const point of this.equityCurve) {
|
||||
const dateKey = point.date.toISOString().split('T')[0];
|
||||
dailyEquity.set(dateKey, point); // This will keep the last value of each day
|
||||
}
|
||||
|
||||
// Convert to sorted array
|
||||
const dailyPoints = Array.from(dailyEquity.values())
|
||||
.sort((a, b) => a.date.getTime() - b.date.getTime());
|
||||
|
||||
// Calculate returns from daily points
|
||||
for (let i = 1; i < dailyPoints.length; i++) {
|
||||
const prevValue = dailyPoints[i - 1].value;
|
||||
const currValue = dailyPoints[i].value;
|
||||
if (prevValue > 0) {
|
||||
this.dailyReturns.push((currValue - prevValue) / prevValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -315,13 +334,15 @@ export class PerformanceAnalyzer {
|
|||
|
||||
private calculateVolatility(): number {
|
||||
if (this.dailyReturns.length === 0) return 0;
|
||||
return stats.standardDeviation(this.dailyReturns) * Math.sqrt(252) * 100;
|
||||
// Return volatility as a decimal percentage (not multiplied by 100)
|
||||
return stats.standardDeviation(this.dailyReturns) * Math.sqrt(252);
|
||||
}
|
||||
|
||||
private calculateDownsideVolatility(): number {
|
||||
const negativeReturns = this.dailyReturns.filter(r => r < 0);
|
||||
if (negativeReturns.length === 0) return 0;
|
||||
return stats.standardDeviation(negativeReturns) * Math.sqrt(252) * 100;
|
||||
// Return volatility as a decimal percentage (not multiplied by 100)
|
||||
return stats.standardDeviation(negativeReturns) * Math.sqrt(252);
|
||||
}
|
||||
|
||||
private calculateVaR(): { var95: number; cvar95: number } {
|
||||
|
|
@ -330,20 +351,30 @@ export class PerformanceAnalyzer {
|
|||
const sortedReturns = [...this.dailyReturns].sort((a, b) => a - b);
|
||||
const index95 = Math.floor(sortedReturns.length * 0.05);
|
||||
|
||||
// Handle edge case where we don't have enough data points
|
||||
if (index95 === 0 || sortedReturns.length < 20) {
|
||||
return { var95: 0, cvar95: 0 };
|
||||
}
|
||||
|
||||
const var95 = Math.abs(sortedReturns[index95]) * 100;
|
||||
const cvar95 = Math.abs(stats.mean(sortedReturns.slice(0, index95))) * 100;
|
||||
const tailReturns = sortedReturns.slice(0, index95);
|
||||
const cvar95 = tailReturns.length > 0 ? Math.abs(stats.mean(tailReturns)) * 100 : 0;
|
||||
|
||||
return { var95, cvar95 };
|
||||
}
|
||||
|
||||
private calculateSharpeRatio(annualReturn: number, volatility: number, riskFree: number = 2): number {
|
||||
if (volatility === 0) return 0;
|
||||
return (annualReturn - riskFree) / volatility;
|
||||
// Both annualReturn and volatility should be in percentage form
|
||||
const volatilityPct = volatility * 100;
|
||||
return (annualReturn - riskFree) / volatilityPct;
|
||||
}
|
||||
|
||||
private calculateSortinoRatio(annualReturn: number, downVolatility: number, riskFree: number = 2): number {
|
||||
if (downVolatility === 0) return 0;
|
||||
return (annualReturn - riskFree) / downVolatility;
|
||||
// Both annualReturn and downVolatility should be in percentage form
|
||||
const downVolatilityPct = downVolatility * 100;
|
||||
return (annualReturn - riskFree) / downVolatilityPct;
|
||||
}
|
||||
|
||||
private calculateInformationRatio(): number {
|
||||
|
|
@ -380,8 +411,13 @@ export class PerformanceAnalyzer {
|
|||
};
|
||||
}
|
||||
|
||||
const wins = this.trades.filter(t => t.pnl > 0);
|
||||
const losses = this.trades.filter(t => t.pnl < 0);
|
||||
// Filter out trades with 0 or undefined P&L
|
||||
const validTrades = this.trades.filter(t => t.pnl !== undefined && t.pnl !== null);
|
||||
const wins = validTrades.filter(t => t.pnl > 0);
|
||||
const losses = validTrades.filter(t => t.pnl < 0);
|
||||
|
||||
// Log trade analysis for debugging
|
||||
logger.debug(`Trade Analysis: Total=${this.trades.length}, Valid=${validTrades.length}, Wins=${wins.length}, Losses=${losses.length}`);
|
||||
|
||||
const totalWins = wins.reduce((sum, t) => sum + t.pnl, 0);
|
||||
const totalLosses = Math.abs(losses.reduce((sum, t) => sum + t.pnl, 0));
|
||||
|
|
@ -389,7 +425,7 @@ export class PerformanceAnalyzer {
|
|||
const avgWin = wins.length > 0 ? totalWins / wins.length : 0;
|
||||
const avgLoss = losses.length > 0 ? totalLosses / losses.length : 0;
|
||||
|
||||
const winRate = (wins.length / this.trades.length) * 100;
|
||||
const winRate = validTrades.length > 0 ? (wins.length / validTrades.length) * 100 : 0;
|
||||
const profitFactor = totalLosses > 0 ? totalWins / totalLosses : totalWins > 0 ? Infinity : 0;
|
||||
const expectancy = (winRate / 100 * avgWin) - ((100 - winRate) / 100 * avgLoss);
|
||||
const payoffRatio = avgLoss > 0 ? avgWin / avgLoss : 0;
|
||||
|
|
|
|||
|
|
@ -99,6 +99,8 @@ export class BacktestEngine extends EventEmitter {
|
|||
private microstructures: Map<string, MarketMicrostructure> = new Map();
|
||||
private container: IServiceContainer;
|
||||
private initialCapital: number = 100000;
|
||||
private commission: number = 0.001; // Default 0.1%
|
||||
private slippage: number = 0.0001; // Default 0.01%
|
||||
private pendingOrders: Map<string, any> = new Map();
|
||||
private ordersListenerSetup = false;
|
||||
|
||||
|
|
@ -116,7 +118,8 @@ export class BacktestEngine extends EventEmitter {
|
|||
includeDarkPools: true,
|
||||
latencyMs: 1
|
||||
});
|
||||
this.performanceAnalyzer = new PerformanceAnalyzer();
|
||||
// Don't create performance analyzer here - wait for runBacktest
|
||||
// this.performanceAnalyzer = new PerformanceAnalyzer(this.initialCapital);
|
||||
|
||||
// Set up order listener immediately
|
||||
this.setupOrderListener();
|
||||
|
|
@ -132,6 +135,11 @@ export class BacktestEngine extends EventEmitter {
|
|||
this.reset();
|
||||
this.isRunning = true;
|
||||
this.initialCapital = validatedConfig.initialCapital;
|
||||
this.commission = validatedConfig.commission || 0.001;
|
||||
this.slippage = validatedConfig.slippage || 0.0001;
|
||||
|
||||
// Recreate performance analyzer with correct initial capital
|
||||
this.performanceAnalyzer = new PerformanceAnalyzer(this.initialCapital);
|
||||
|
||||
// Initialize equity curve with starting capital
|
||||
this.equityCurve.push({
|
||||
|
|
@ -145,6 +153,8 @@ export class BacktestEngine extends EventEmitter {
|
|||
this.initialCapital
|
||||
);
|
||||
|
||||
this.container.logger.info(`[BacktestEngine] Initial capital set to ${this.initialCapital}, equity curve initialized with ${this.equityCurve.length} points`);
|
||||
|
||||
// Generate backtest ID
|
||||
const backtestId = `backtest_${Date.now()}`;
|
||||
|
||||
|
|
@ -668,12 +678,16 @@ export class BacktestEngine extends EventEmitter {
|
|||
private async updateEquityCurve(): Promise<void> {
|
||||
const totalEquity = await this.getPortfolioValue();
|
||||
|
||||
this.container.logger.debug(`[updateEquityCurve] currentTime=${this.currentTime}, totalEquity=${totalEquity}, initialCapital=${this.initialCapital}`);
|
||||
|
||||
// Don't add duplicate points at the same timestamp
|
||||
const lastPoint = this.equityCurve[this.equityCurve.length - 1];
|
||||
if (lastPoint && lastPoint.timestamp === this.currentTime) {
|
||||
// Update the value instead of adding a duplicate
|
||||
this.container.logger.debug(`[updateEquityCurve] Updating existing point at ${this.currentTime}: ${lastPoint.value} -> ${totalEquity}`);
|
||||
lastPoint.value = totalEquity;
|
||||
} else {
|
||||
this.container.logger.debug(`[updateEquityCurve] Adding new point at ${this.currentTime}: ${totalEquity}`);
|
||||
this.equityCurve.push({
|
||||
timestamp: this.currentTime,
|
||||
value: totalEquity
|
||||
|
|
@ -684,6 +698,7 @@ export class BacktestEngine extends EventEmitter {
|
|||
private async getPortfolioValue(): Promise<number> {
|
||||
const tradingEngine = this.strategyManager.getTradingEngine();
|
||||
if (!tradingEngine) {
|
||||
this.container.logger.debug(`[getPortfolioValue] No trading engine, returning initialCapital: ${this.initialCapital}`);
|
||||
return this.initialCapital;
|
||||
}
|
||||
|
||||
|
|
@ -692,9 +707,12 @@ export class BacktestEngine extends EventEmitter {
|
|||
const [realized, unrealized] = tradingEngine.getTotalPnl();
|
||||
const totalValue = this.initialCapital + realized + unrealized;
|
||||
|
||||
this.container.logger.debug(`[getPortfolioValue] P&L: realized=${realized}, unrealized=${unrealized}, initialCapital=${this.initialCapital}, totalValue=${totalValue}`);
|
||||
|
||||
// Ensure we never return 0 or negative values that would cause spikes
|
||||
// This handles the case where getTotalPnl might not be initialized yet
|
||||
if (totalValue <= 0 || isNaN(totalValue)) {
|
||||
this.container.logger.debug(`[getPortfolioValue] Invalid totalValue, returning initialCapital: ${this.initialCapital}`);
|
||||
return this.initialCapital;
|
||||
}
|
||||
|
||||
|
|
@ -941,13 +959,6 @@ export class BacktestEngine extends EventEmitter {
|
|||
return profile.map(v => v / sum);
|
||||
}
|
||||
|
||||
private getPortfolioValue(): number {
|
||||
const tradingEngine = this.strategyManager.getTradingEngine();
|
||||
if (!tradingEngine) {return 100000;} // Default initial capital
|
||||
|
||||
const [realized, unrealized] = tradingEngine.getTotalPnl();
|
||||
return 100000 + realized + unrealized;
|
||||
}
|
||||
|
||||
async stopBacktest(): Promise<void> {
|
||||
this.isRunning = false;
|
||||
|
|
@ -1160,8 +1171,8 @@ export class BacktestEngine extends EventEmitter {
|
|||
if (!orderEvent) continue;
|
||||
|
||||
const fillPrice = orderEvent.order.side === 'buy' ?
|
||||
currentPrice * (1 + (this.marketSimulator.config.slippage || 0.0001)) :
|
||||
currentPrice * (1 - (this.marketSimulator.config.slippage || 0.0001));
|
||||
currentPrice * (1 + this.slippage) :
|
||||
currentPrice * (1 - this.slippage);
|
||||
|
||||
const fill = {
|
||||
orderId,
|
||||
|
|
@ -1170,7 +1181,7 @@ export class BacktestEngine extends EventEmitter {
|
|||
quantity: orderEvent.order.quantity,
|
||||
price: fillPrice,
|
||||
timestamp: this.currentTime,
|
||||
commission: orderEvent.order.quantity * fillPrice * 0.001, // 0.1% commission
|
||||
commission: orderEvent.order.quantity * fillPrice * this.commission,
|
||||
strategyId: orderEvent.strategyId
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -64,7 +64,8 @@ export class ModeManager extends EventEmitter {
|
|||
return {
|
||||
startTime: new Date(config.startDate).getTime(),
|
||||
endTime: new Date(config.endDate).getTime(),
|
||||
speedMultiplier: this.getSpeedMultiplier(config.speed)
|
||||
speedMultiplier: this.getSpeedMultiplier(config.speed),
|
||||
initialCapital: config.initialCapital
|
||||
};
|
||||
case 'paper':
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -202,8 +202,9 @@ export abstract class BaseStrategy extends EventEmitter {
|
|||
}
|
||||
|
||||
protected getEquity(): number {
|
||||
// Simplified - in reality would calculate based on positions and market values
|
||||
return 100000 + this.performance.totalPnl; // Assuming 100k starting capital
|
||||
// Get initial capital from config parameters or default
|
||||
const initialCapital = this.config.parameters?.initialCapital || 100000;
|
||||
return initialCapital + this.performance.totalPnl;
|
||||
}
|
||||
|
||||
protected async signalToOrder(signal: Signal): Promise<OrderRequest | null> {
|
||||
|
|
|
|||
|
|
@ -272,13 +272,14 @@ export class SimpleMovingAverageCrossover extends BaseStrategy {
|
|||
}
|
||||
|
||||
// Try to get account balance from trading engine
|
||||
let accountBalance = 100000; // Default
|
||||
// Use initial capital from config parameters if available
|
||||
let accountBalance = this.config.parameters?.initialCapital || 100000;
|
||||
try {
|
||||
if (tradingEngine.getAccountBalance) {
|
||||
accountBalance = tradingEngine.getAccountBalance();
|
||||
} else if (tradingEngine.getTotalPnl) {
|
||||
const [realized, unrealized] = tradingEngine.getTotalPnl();
|
||||
accountBalance = 100000 + realized + unrealized; // Assuming 100k initial
|
||||
accountBalance = (this.config.parameters?.initialCapital || 100000) + realized + unrealized;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('Could not get account balance:', error);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue