cleanup and fixed initial capital / commision / splippage

This commit is contained in:
Boki 2025-07-03 19:39:19 -04:00
parent 70da2c68e5
commit d14380d740
19 changed files with 1344 additions and 33 deletions

View file

@ -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;

View file

@ -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
};

View file

@ -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 {

View file

@ -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> {

View file

@ -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);