diff --git a/examples/neural-trader/system/backtesting.js b/examples/neural-trader/system/backtesting.js index 1e227ad4..8ded321d 100644 --- a/examples/neural-trader/system/backtesting.js +++ b/examples/neural-trader/system/backtesting.js @@ -46,252 +46,211 @@ class PerformanceMetrics { this.dailyRiskFreeRate = Math.pow(1 + riskFreeRate, 1/252) - 1; } - // Calculate all metrics from equity curve + // Optimized: Calculate all metrics with minimal passes over data calculate(equityCurve, benchmark = null) { if (equityCurve.length < 2) { return this.emptyMetrics(); } - const returns = this.calculateReturns(equityCurve); - const benchmarkReturns = benchmark ? this.calculateReturns(benchmark) : null; + // Single pass: compute returns and statistics together + const n = equityCurve.length; + const returns = new Array(n - 1); + let sum = 0, sumSq = 0; + let positiveSum = 0, negativeSum = 0; + let positiveCount = 0, negativeCount = 0; + let compoundReturn = 1; + + for (let i = 1; i < n; i++) { + const r = (equityCurve[i] - equityCurve[i-1]) / equityCurve[i-1]; + returns[i-1] = r; + sum += r; + sumSq += r * r; + compoundReturn *= (1 + r); + if (r > 0) { positiveSum += r; positiveCount++; } + else if (r < 0) { negativeSum += r; negativeCount++; } + } + + const mean = sum / returns.length; + const variance = sumSq / returns.length - mean * mean; + const volatility = Math.sqrt(variance); + const annualizedVol = volatility * Math.sqrt(252); + + // Single pass: drawdown metrics + const ddMetrics = this.computeDrawdownMetrics(equityCurve); + + // Pre-computed stats for Sharpe/Sortino + const excessMean = mean - this.dailyRiskFreeRate; + const sharpe = volatility > 0 ? (excessMean / volatility) * Math.sqrt(252) : 0; + + // Downside deviation (single pass) + let downsideVariance = 0; + for (let i = 0; i < returns.length; i++) { + const excess = returns[i] - this.dailyRiskFreeRate; + if (excess < 0) downsideVariance += excess * excess; + } + const downsideDeviation = Math.sqrt(downsideVariance / returns.length); + const sortino = downsideDeviation > 0 ? (excessMean / downsideDeviation) * Math.sqrt(252) : 0; + + // Annualized return + const years = returns.length / 252; + const annualizedReturn = Math.pow(compoundReturn, 1 / years) - 1; + + // CAGR + const cagr = Math.pow(equityCurve[n-1] / equityCurve[0], 1 / years) - 1; + + // Calmar + const calmar = ddMetrics.maxDrawdown > 0 ? annualizedReturn / ddMetrics.maxDrawdown : 0; + + // Trade metrics (using pre-computed counts) + const winRate = returns.length > 0 ? positiveCount / returns.length : 0; + const avgWin = positiveCount > 0 ? positiveSum / positiveCount : 0; + const avgLoss = negativeCount > 0 ? negativeSum / negativeCount : 0; + const profitFactor = negativeSum !== 0 ? positiveSum / Math.abs(negativeSum) : Infinity; + const payoffRatio = avgLoss !== 0 ? avgWin / Math.abs(avgLoss) : Infinity; + const expectancy = winRate * avgWin - (1 - winRate) * Math.abs(avgLoss); + + // VaR (requires sort - do lazily) + const sortedReturns = [...returns].sort((a, b) => a - b); + const var95 = -sortedReturns[Math.floor(0.05 * sortedReturns.length)]; + const var99 = -sortedReturns[Math.floor(0.01 * sortedReturns.length)]; + + // CVaR + const tailIndex = Math.floor(0.05 * sortedReturns.length); + let cvarSum = 0; + for (let i = 0; i <= tailIndex; i++) cvarSum += sortedReturns[i]; + const cvar95 = tailIndex > 0 ? -cvarSum / (tailIndex + 1) : 0; + + // Skewness and Kurtosis (using pre-computed mean/variance) + let m3 = 0, m4 = 0; + for (let i = 0; i < returns.length; i++) { + const d = returns[i] - mean; + const d2 = d * d; + m3 += d * d2; + m4 += d2 * d2; + } + m3 /= returns.length; + m4 /= returns.length; + const std = volatility; + const skewness = std > 0 ? m3 / (std * std * std) : 0; + const kurtosis = std > 0 ? m4 / (std * std * std * std) - 3 : 0; + + // Best/worst day + let bestDay = returns[0], worstDay = returns[0]; + for (let i = 1; i < returns.length; i++) { + if (returns[i] > bestDay) bestDay = returns[i]; + if (returns[i] < worstDay) worstDay = returns[i]; + } + + // Benchmark metrics + let informationRatio = null; + if (benchmark) { + informationRatio = this.informationRatioFast(returns, benchmark); + } return { - // Return metrics - totalReturn: this.totalReturn(equityCurve), - annualizedReturn: this.annualizedReturn(returns), - cagr: this.cagr(equityCurve), - - // Risk metrics - volatility: this.volatility(returns), - annualizedVolatility: this.annualizedVolatility(returns), - maxDrawdown: this.maxDrawdown(equityCurve), - averageDrawdown: this.averageDrawdown(equityCurve), - drawdownDuration: this.drawdownDuration(equityCurve), - - // Risk-adjusted metrics - sharpeRatio: this.sharpeRatio(returns), - sortinoRatio: this.sortinoRatio(returns), - calmarRatio: this.calmarRatio(equityCurve, returns), - informationRatio: benchmarkReturns ? this.informationRatio(returns, benchmarkReturns) : null, - - // Trade metrics - winRate: this.winRate(returns), - profitFactor: this.profitFactor(returns), - averageWin: this.averageWin(returns), - averageLoss: this.averageLoss(returns), - payoffRatio: this.payoffRatio(returns), - expectancy: this.expectancy(returns), - - // Tail risk metrics - var95: this.valueAtRisk(returns, 0.95), - var99: this.valueAtRisk(returns, 0.99), - cvar95: this.conditionalVaR(returns, 0.95), - skewness: this.skewness(returns), - kurtosis: this.kurtosis(returns), - - // Additional metrics + totalReturn: compoundReturn - 1, + annualizedReturn, + cagr, + volatility, + annualizedVolatility: annualizedVol, + maxDrawdown: ddMetrics.maxDrawdown, + averageDrawdown: ddMetrics.averageDrawdown, + drawdownDuration: ddMetrics.maxDuration, + sharpeRatio: sharpe, + sortinoRatio: sortino, + calmarRatio: calmar, + informationRatio, + winRate, + profitFactor, + averageWin: avgWin, + averageLoss: avgLoss, + payoffRatio, + expectancy, + var95, + var99, + cvar95, + skewness, + kurtosis, tradingDays: returns.length, - bestDay: Math.max(...returns), - worstDay: Math.min(...returns), - positiveMonths: this.positiveMonths(returns), - - // Raw data + bestDay, + worstDay, + positiveMonths: this.positiveMonthsFast(returns), returns, equityCurve }; } - calculateReturns(equityCurve) { - const returns = []; - for (let i = 1; i < equityCurve.length; i++) { - returns.push((equityCurve[i] - equityCurve[i-1]) / equityCurve[i-1]); - } - return returns; - } - - totalReturn(equityCurve) { - return (equityCurve[equityCurve.length - 1] - equityCurve[0]) / equityCurve[0]; - } - - annualizedReturn(returns) { - const totalReturn = returns.reduce((a, b) => (1 + a) * (1 + b), 1) - 1; - const years = returns.length / 252; - return Math.pow(1 + totalReturn, 1 / years) - 1; - } - - cagr(equityCurve) { - const years = (equityCurve.length - 1) / 252; - return Math.pow(equityCurve[equityCurve.length - 1] / equityCurve[0], 1 / years) - 1; - } - - volatility(returns) { - const mean = returns.reduce((a, b) => a + b, 0) / returns.length; - const variance = returns.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / returns.length; - return Math.sqrt(variance); - } - - annualizedVolatility(returns) { - return this.volatility(returns) * Math.sqrt(252); - } - - maxDrawdown(equityCurve) { + // Optimized: Single pass drawdown computation + computeDrawdownMetrics(equityCurve) { let maxDrawdown = 0; let peak = equityCurve[0]; - - for (const value of equityCurve) { - if (value > peak) peak = value; - const drawdown = (peak - value) / peak; - if (drawdown > maxDrawdown) maxDrawdown = drawdown; - } - - return maxDrawdown; - } - - averageDrawdown(equityCurve) { - const drawdowns = []; - let peak = equityCurve[0]; - - for (const value of equityCurve) { - if (value > peak) peak = value; - drawdowns.push((peak - value) / peak); - } - - return drawdowns.reduce((a, b) => a + b, 0) / drawdowns.length; - } - - drawdownDuration(equityCurve) { + let ddSum = 0; let maxDuration = 0; let currentDuration = 0; - let peak = equityCurve[0]; - for (const value of equityCurve) { - if (value >= peak) { + for (let i = 0; i < equityCurve.length; i++) { + const value = equityCurve[i]; + if (value > peak) { peak = value; currentDuration = 0; } else { currentDuration++; if (currentDuration > maxDuration) maxDuration = currentDuration; } + const dd = (peak - value) / peak; + ddSum += dd; + if (dd > maxDrawdown) maxDrawdown = dd; } - return maxDuration; + return { + maxDrawdown, + averageDrawdown: ddSum / equityCurve.length, + maxDuration + }; } - sharpeRatio(returns) { - const excessReturns = returns.map(r => r - this.dailyRiskFreeRate); - const meanExcess = excessReturns.reduce((a, b) => a + b, 0) / excessReturns.length; - const vol = this.volatility(excessReturns); - return vol > 0 ? (meanExcess / vol) * Math.sqrt(252) : 0; - } - - sortinoRatio(returns) { - const excessReturns = returns.map(r => r - this.dailyRiskFreeRate); - const meanExcess = excessReturns.reduce((a, b) => a + b, 0) / excessReturns.length; - - // Downside deviation - const negativeReturns = excessReturns.filter(r => r < 0); - if (negativeReturns.length === 0) return Infinity; - - const downsideVariance = negativeReturns.reduce((a, b) => a + b * b, 0) / returns.length; - const downsideDeviation = Math.sqrt(downsideVariance); - - return downsideDeviation > 0 ? (meanExcess / downsideDeviation) * Math.sqrt(252) : 0; - } - - calmarRatio(equityCurve, returns) { - const annReturn = this.annualizedReturn(returns); - const maxDD = this.maxDrawdown(equityCurve); - return maxDD > 0 ? annReturn / maxDD : 0; - } - - informationRatio(returns, benchmarkReturns) { - const trackingError = []; + // Optimized information ratio + informationRatioFast(returns, benchmark) { + const benchmarkReturns = this.calculateReturns(benchmark); const minLen = Math.min(returns.length, benchmarkReturns.length); + let sum = 0, sumSq = 0; for (let i = 0; i < minLen; i++) { - trackingError.push(returns[i] - benchmarkReturns[i]); + const te = returns[i] - benchmarkReturns[i]; + sum += te; + sumSq += te * te; } - const meanTE = trackingError.reduce((a, b) => a + b, 0) / trackingError.length; - const teVol = this.volatility(trackingError); - - return teVol > 0 ? (meanTE / teVol) * Math.sqrt(252) : 0; + const mean = sum / minLen; + const variance = sumSq / minLen - mean * mean; + const vol = Math.sqrt(variance); + return vol > 0 ? (mean / vol) * Math.sqrt(252) : 0; } - winRate(returns) { - const wins = returns.filter(r => r > 0).length; - return returns.length > 0 ? wins / returns.length : 0; - } + // Optimized positive months + positiveMonthsFast(returns) { + let positiveMonths = 0; + let totalMonths = 0; + let monthReturn = 1; - profitFactor(returns) { - const grossProfit = returns.filter(r => r > 0).reduce((a, b) => a + b, 0); - const grossLoss = Math.abs(returns.filter(r => r < 0).reduce((a, b) => a + b, 0)); - return grossLoss > 0 ? grossProfit / grossLoss : Infinity; - } - - averageWin(returns) { - const wins = returns.filter(r => r > 0); - return wins.length > 0 ? wins.reduce((a, b) => a + b, 0) / wins.length : 0; - } - - averageLoss(returns) { - const losses = returns.filter(r => r < 0); - return losses.length > 0 ? losses.reduce((a, b) => a + b, 0) / losses.length : 0; - } - - payoffRatio(returns) { - const avgWin = this.averageWin(returns); - const avgLoss = Math.abs(this.averageLoss(returns)); - return avgLoss > 0 ? avgWin / avgLoss : Infinity; - } - - expectancy(returns) { - const winRate = this.winRate(returns); - const avgWin = this.averageWin(returns); - const avgLoss = Math.abs(this.averageLoss(returns)); - return winRate * avgWin - (1 - winRate) * avgLoss; - } - - valueAtRisk(returns, confidence = 0.95) { - const sorted = [...returns].sort((a, b) => a - b); - const index = Math.floor((1 - confidence) * sorted.length); - return -sorted[index]; - } - - conditionalVaR(returns, confidence = 0.95) { - const sorted = [...returns].sort((a, b) => a - b); - const index = Math.floor((1 - confidence) * sorted.length); - const tailReturns = sorted.slice(0, index + 1); - return tailReturns.length > 0 ? -tailReturns.reduce((a, b) => a + b, 0) / tailReturns.length : 0; - } - - skewness(returns) { - const n = returns.length; - const mean = returns.reduce((a, b) => a + b, 0) / n; - const m2 = returns.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / n; - const m3 = returns.reduce((a, b) => a + Math.pow(b - mean, 3), 0) / n; - const std = Math.sqrt(m2); - return std > 0 ? m3 / Math.pow(std, 3) : 0; - } - - kurtosis(returns) { - const n = returns.length; - const mean = returns.reduce((a, b) => a + b, 0) / n; - const m2 = returns.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / n; - const m4 = returns.reduce((a, b) => a + Math.pow(b - mean, 4), 0) / n; - const std = Math.sqrt(m2); - return std > 0 ? m4 / Math.pow(std, 4) - 3 : 0; // Excess kurtosis - } - - positiveMonths(returns) { - // Group by 21-day periods (approximate months) - const monthlyReturns = []; - for (let i = 0; i < returns.length; i += 21) { - const monthReturn = returns.slice(i, i + 21).reduce((a, b) => (1 + a) * (1 + b) - 1, 0); - monthlyReturns.push(monthReturn); + for (let i = 0; i < returns.length; i++) { + monthReturn *= (1 + returns[i]); + if ((i + 1) % 21 === 0 || i === returns.length - 1) { + if (monthReturn > 1) positiveMonths++; + totalMonths++; + monthReturn = 1; + } } - const positive = monthlyReturns.filter(r => r > 0).length; - return monthlyReturns.length > 0 ? positive / monthlyReturns.length : 0; + + return totalMonths > 0 ? positiveMonths / totalMonths : 0; + } + + calculateReturns(equityCurve) { + const returns = new Array(equityCurve.length - 1); + for (let i = 1; i < equityCurve.length; i++) { + returns[i-1] = (equityCurve[i] - equityCurve[i-1]) / equityCurve[i-1]; + } + return returns; } emptyMetrics() { diff --git a/examples/neural-trader/system/risk-management.js b/examples/neural-trader/system/risk-management.js index 36a3f6f0..a33413b6 100644 --- a/examples/neural-trader/system/risk-management.js +++ b/examples/neural-trader/system/risk-management.js @@ -172,9 +172,19 @@ class CircuitBreaker { // Tracking data this.peakEquity = 0; this.currentEquity = 0; - this.recentTrades = []; - this.dailyVolatility = []; this.consecutiveLosses = 0; + + // Optimized: Use ring buffers instead of arrays with shift/slice + const tradeWindowSize = config.lossRateWindow * 2; + this._tradeBuffer = new Array(tradeWindowSize); + this._tradeIndex = 0; + this._tradeCount = 0; + this._tradeLossCount = 0; // Track losses incrementally + + this._volBuffer = new Array(20); + this._volIndex = 0; + this._volCount = 0; + this._volSum = 0; // Running sum for O(1) average } // Update with new equity value @@ -191,18 +201,26 @@ class CircuitBreaker { } } - // Record a trade result + // Optimized: Record trade with O(1) ring buffer recordTrade(profit) { - this.recentTrades.push({ - profit, - timestamp: Date.now() - }); + const bufferSize = this._tradeBuffer.length; + const windowSize = this.config.lossRateWindow; - // Keep only recent trades - if (this.recentTrades.length > this.config.lossRateWindow * 2) { - this.recentTrades = this.recentTrades.slice(-this.config.lossRateWindow); + // If overwriting an old trade, adjust loss count + if (this._tradeCount >= bufferSize) { + const oldTrade = this._tradeBuffer[this._tradeIndex]; + if (oldTrade && oldTrade.profit < 0) { + this._tradeLossCount--; + } } + // Add new trade + this._tradeBuffer[this._tradeIndex] = { profit, timestamp: Date.now() }; + if (profit < 0) this._tradeLossCount++; + + this._tradeIndex = (this._tradeIndex + 1) % bufferSize; + if (this._tradeCount < bufferSize) this._tradeCount++; + // Update consecutive losses if (profit < 0) { this.consecutiveLosses++; @@ -210,11 +228,18 @@ class CircuitBreaker { this.consecutiveLosses = 0; } - // Check loss rate breaker - const recentWindow = this.recentTrades.slice(-this.config.lossRateWindow); - if (recentWindow.length >= this.config.lossRateWindow) { - const losses = recentWindow.filter(t => t.profit < 0).length; - const lossRate = losses / recentWindow.length; + // Check loss rate breaker (O(1) using tracked count) + if (this._tradeCount >= windowSize) { + // Count losses in recent window + let recentLosses = 0; + const startIdx = (this._tradeIndex - windowSize + bufferSize) % bufferSize; + for (let i = 0; i < windowSize; i++) { + const idx = (startIdx + i) % bufferSize; + if (this._tradeBuffer[idx] && this._tradeBuffer[idx].profit < 0) { + recentLosses++; + } + } + const lossRate = recentLosses / windowSize; if (lossRate >= this.config.lossRateThreshold) { this.trip('lossRate', `Loss rate ${(lossRate * 100).toFixed(1)}% exceeds threshold`); @@ -227,21 +252,28 @@ class CircuitBreaker { } } - // Update daily volatility + // Optimized: Update volatility with O(1) ring buffer and running sum updateVolatility(dailyReturn) { - this.dailyVolatility.push(Math.abs(dailyReturn)); + const absReturn = Math.abs(dailyReturn); + const bufferSize = this._volBuffer.length; - // Keep rolling window - if (this.dailyVolatility.length > 20) { - this.dailyVolatility.shift(); + // If overwriting old value, subtract from running sum + if (this._volCount >= bufferSize) { + this._volSum -= this._volBuffer[this._volIndex]; } - // Calculate average volatility - if (this.dailyVolatility.length >= 5) { - const avgVol = this.dailyVolatility.slice(0, -1).reduce((a, b) => a + b, 0) / (this.dailyVolatility.length - 1); - const currentVol = this.dailyVolatility[this.dailyVolatility.length - 1]; + // Add new value + this._volBuffer[this._volIndex] = absReturn; + this._volSum += absReturn; + + this._volIndex = (this._volIndex + 1) % bufferSize; + if (this._volCount < bufferSize) this._volCount++; + + // Check volatility spike (O(1) using running sum) + if (this._volCount >= 5) { + const avgVol = (this._volSum - absReturn) / (this._volCount - 1); + const currentVol = absReturn; - // Check volatility spike if (currentVol > avgVol * this.config.volatilityMultiplier || currentVol > this.config.volatilityThreshold) { this.trip('volatility', `Volatility spike: ${(currentVol * 100).toFixed(2)}%`); @@ -298,7 +330,13 @@ class CircuitBreaker { forceReset() { this.reset(); this.peakEquity = this.currentEquity; - this.recentTrades = []; + // Reset ring buffers + this._tradeIndex = 0; + this._tradeCount = 0; + this._tradeLossCount = 0; + this._volIndex = 0; + this._volCount = 0; + this._volSum = 0; } getState() { @@ -310,10 +348,24 @@ class CircuitBreaker { }; } + // Optimized: O(windowSize) but only called for reporting calculateRecentLossRate() { - const recent = this.recentTrades.slice(-this.config.lossRateWindow); - if (recent.length === 0) return 0; - return recent.filter(t => t.profit < 0).length / recent.length; + const windowSize = this.config.lossRateWindow; + const count = Math.min(this._tradeCount, windowSize); + if (count === 0) return 0; + + let losses = 0; + const bufferSize = this._tradeBuffer.length; + const startIdx = (this._tradeIndex - count + bufferSize) % bufferSize; + + for (let i = 0; i < count; i++) { + const idx = (startIdx + i) % bufferSize; + if (this._tradeBuffer[idx] && this._tradeBuffer[idx].profit < 0) { + losses++; + } + } + + return losses / count; } }