From a1c82ae0b81ca7b4906fffecdc92a861879e058b Mon Sep 17 00:00:00 2001 From: Bojan Kucera Date: Wed, 4 Jun 2025 19:27:11 -0400 Subject: [PATCH] added more functions --- .../src/calculations/market-statistics.ts | 135 ++++++++++++- .../src/calculations/portfolio-analytics.ts | 189 ++++++++++++++++-- .../utils/src/calculations/position-sizing.ts | 70 +++++++ libs/utils/src/calculations/risk-metrics.ts | 126 ++++++++++++ .../src/calculations/technical-indicators.ts | 148 ++++++++++++++ libs/utils/test/simple-test.ts | 19 -- libs/utils/test/validation.ts | 138 ------------- 7 files changed, 648 insertions(+), 177 deletions(-) delete mode 100644 libs/utils/test/simple-test.ts delete mode 100644 libs/utils/test/validation.ts diff --git a/libs/utils/src/calculations/market-statistics.ts b/libs/utils/src/calculations/market-statistics.ts index 666fd7b..5efb214 100644 --- a/libs/utils/src/calculations/market-statistics.ts +++ b/libs/utils/src/calculations/market-statistics.ts @@ -326,10 +326,9 @@ export function IntradayPatterns( hourlyVolume[hour] = data.volumes.length > 0 ? data.volumes.reduce((sum, vol) => sum + vol, 0) / data.volumes.length : 0; } - - // opening gap and closing drift + // opening gap and closing drift const openingGap = ohlcv.length > 1 ? - (ohlcv[0].open - ohlcv[0].close) / ohlcv[0].close : 0; + (ohlcv[1].open - ohlcv[0].close) / ohlcv[0].close : 0; const lastCandle = ohlcv[ohlcv.length - 1]; const closingDrift = (lastCandle.close - lastCandle.open) / lastCandle.open; @@ -536,6 +535,136 @@ export function ImplementationShortfall( }; } +/** + * Amihud Illiquidity Measure (price impact per unit of volume) + */ +export function amihudIlliquidity( + ohlcv: OHLCVData[], + lookbackPeriod: number = 252 +): number { + if (ohlcv.length < lookbackPeriod) return 0; + + const recentData = ohlcv.slice(-lookbackPeriod); + let illiquiditySum = 0; + let validDays = 0; + + for (const candle of recentData) { + if (candle.volume > 0) { + const dailyReturn = Math.abs((candle.close - candle.open) / candle.open); + const dollarVolume = candle.volume * candle.close; + + if (dollarVolume > 0) { + illiquiditySum += dailyReturn / dollarVolume; + validDays++; + } + } + } + + return validDays > 0 ? (illiquiditySum / validDays) * 1000000 : 0; // Scale to millions +} + +/** + * Roll's Spread Estimator (effective spread from serial covariance) + */ +export function rollSpreadEstimator(prices: number[]): number { + if (prices.length < 3) return 0; + + // Calculate price changes + const priceChanges: number[] = []; + for (let i = 1; i < prices.length; i++) { + priceChanges.push(prices[i] - prices[i - 1]); + } + + // Calculate serial covariance + let covariance = 0; + for (let i = 1; i < priceChanges.length; i++) { + covariance += priceChanges[i] * priceChanges[i - 1]; + } + covariance /= (priceChanges.length - 1); + + // Roll's estimator: spread = 2 * sqrt(-covariance) + const spread = covariance < 0 ? 2 * Math.sqrt(-covariance) : 0; + + return spread; +} + +/** + * Kyle's Lambda (price impact coefficient) + */ +export function kyleLambda( + priceChanges: number[], + orderFlow: number[] // Signed order flow (positive for buys, negative for sells) +): number { + if (priceChanges.length !== orderFlow.length || priceChanges.length < 2) return 0; + + // Calculate regression: priceChange = lambda * orderFlow + error + const n = priceChanges.length; + const meanPrice = priceChanges.reduce((sum, p) => sum + p, 0) / n; + const meanFlow = orderFlow.reduce((sum, f) => sum + f, 0) / n; + + let numerator = 0; + let denominator = 0; + + for (let i = 0; i < n; i++) { + const priceDeviation = priceChanges[i] - meanPrice; + const flowDeviation = orderFlow[i] - meanFlow; + + numerator += priceDeviation * flowDeviation; + denominator += flowDeviation * flowDeviation; + } + + return denominator > 0 ? numerator / denominator : 0; +} + +/** + * Probability of Informed Trading (PIN) - simplified version + */ +export function probabilityInformedTrading( + buyVolumes: number[], + sellVolumes: number[], + period: number = 20 +): number { + if (buyVolumes.length !== sellVolumes.length || buyVolumes.length < period) return 0; + + const recentBuys = buyVolumes.slice(-period); + const recentSells = sellVolumes.slice(-period); + + let totalImbalance = 0; + let totalVolume = 0; + + for (let i = 0; i < period; i++) { + const imbalance = Math.abs(recentBuys[i] - recentSells[i]); + const volume = recentBuys[i] + recentSells[i]; + + totalImbalance += imbalance; + totalVolume += volume; + } + + // Simplified PIN estimate based on order imbalance + return totalVolume > 0 ? totalImbalance / totalVolume : 0; +} + +/** + * Herfindahl-Hirschman Index for Volume Concentration + */ +export function volumeConcentrationHHI( + exchanges: Array<{ name: string; volume: number }> +): number { + if (exchanges.length === 0) return 0; + + const totalVolume = exchanges.reduce((sum, exchange) => sum + exchange.volume, 0); + + if (totalVolume === 0) return 0; + + let hhi = 0; + for (const exchange of exchanges) { + const marketShare = exchange.volume / totalVolume; + hhi += marketShare * marketShare; + } + + return hhi * 10000; // Scale to 0-10000 range +} + // Helper functions function calculateVolatility(returns: number[]): number { diff --git a/libs/utils/src/calculations/portfolio-analytics.ts b/libs/utils/src/calculations/portfolio-analytics.ts index 4bbb0b6..a7b045c 100644 --- a/libs/utils/src/calculations/portfolio-analytics.ts +++ b/libs/utils/src/calculations/portfolio-analytics.ts @@ -389,33 +389,188 @@ export function performanceAttribution( } /** - * Calculate efficient frontier points + * Calculate Efficient Frontier points */ export function calculateEfficientFrontier( - expectedReturns: number[], - covarianceMatrix: number[][], - numPoints: number = 100 -): Array<{ return: number; volatility: number; sharpeRatio: number; weights: number[] }> { + returns: number[][], // Array of return series for each asset + symbols: string[], + riskFreeRate: number = 0.02, + numPoints: number = 50 +): Array<{ + weights: number[]; + expectedReturn: number; + volatility: number; + sharpeRatio: number; +}> { + if (returns.length !== symbols.length || returns.length < 2) return []; + + const n = returns.length; + const results: Array<{ weights: number[]; expectedReturn: number; volatility: number; sharpeRatio: number; }> = []; + + // Calculate expected returns and covariance matrix + const expectedReturns = returns.map(assetReturns => + assetReturns.reduce((sum, ret) => sum + ret, 0) / assetReturns.length + ); + + const covarianceMatrix = calculateCovarianceMatrix(returns); + + // Generate target returns from min to max expected return const minReturn = Math.min(...expectedReturns); const maxReturn = Math.max(...expectedReturns); const returnStep = (maxReturn - minReturn) / (numPoints - 1); - const frontierPoints: Array<{ return: number; volatility: number; sharpeRatio: number; weights: number[] }> = []; - for (let i = 0; i < numPoints; i++) { const targetReturn = minReturn + i * returnStep; - // Simplified optimization for target return - // In production, use proper constrained optimization - const result = markowitzOptimization(expectedReturns, covarianceMatrix); + // Find minimum variance portfolio for target return using quadratic programming (simplified) + const weights = findMinimumVarianceWeights(expectedReturns, covarianceMatrix, targetReturn); - frontierPoints.push({ - return: targetReturn, - volatility: result.volatility, - sharpeRatio: result.sharpeRatio, - weights: result.weights - }); + if (weights && weights.length === n) { + const portfolioReturn = weights.reduce((sum, w, j) => sum + w * expectedReturns[j], 0); + const portfolioVariance = calculatePortfolioVariance(weights, covarianceMatrix); + const portfolioVolatility = Math.sqrt(portfolioVariance); + const sharpeRatio = portfolioVolatility > 0 ? (portfolioReturn - riskFreeRate) / portfolioVolatility : 0; + + results.push({ + weights, + expectedReturn: portfolioReturn, + volatility: portfolioVolatility, + sharpeRatio + }); + } } - return frontierPoints; + return results.sort((a, b) => a.volatility - b.volatility); +} + +/** + * Find Minimum Variance Portfolio + */ +export function findMinimumVariancePortfolio( + returns: number[][], + symbols: string[] +): PortfolioOptimizationResult | null { + if (returns.length !== symbols.length || returns.length < 2) return null; + + const covarianceMatrix = calculateCovarianceMatrix(returns); + const n = returns.length; + + // For minimum variance portfolio: w = (Σ^-1 * 1) / (1' * Σ^-1 * 1) + // Simplified implementation using equal weights as starting point + const weights = new Array(n).fill(1 / n); + + // Iterative optimization (simplified) + for (let iter = 0; iter < 100; iter++) { + const gradient = calculateVarianceGradient(weights, covarianceMatrix); + const stepSize = 0.01; + + // Update weights + for (let i = 0; i < n; i++) { + weights[i] -= stepSize * gradient[i]; + } + + // Normalize weights to sum to 1 + const weightSum = weights.reduce((sum, w) => sum + w, 0); + for (let i = 0; i < n; i++) { + weights[i] = Math.max(0, weights[i] / weightSum); + } + } + + const expectedReturns = returns.map(assetReturns => + assetReturns.reduce((sum, ret) => sum + ret, 0) / assetReturns.length + ); + + const portfolioReturn = weights.reduce((sum, w, i) => sum + w * expectedReturns[i], 0); + const portfolioVariance = calculatePortfolioVariance(weights, covarianceMatrix); + const portfolioVolatility = Math.sqrt(portfolioVariance); + const sharpeRatio = portfolioVolatility > 0 ? portfolioReturn / portfolioVolatility : 0; + + return { + weights, + expectedReturn: portfolioReturn, + volatility: portfolioVolatility, + sharpeRatio, + symbols + }; +} + +// Helper functions for portfolio optimization + +function calculateCovarianceMatrix(returns: number[][]): number[][] { + const n = returns.length; + const matrix: number[][] = []; + + for (let i = 0; i < n; i++) { + matrix[i] = []; + for (let j = 0; j < n; j++) { + matrix[i][j] = calculateCovariance(returns[i], returns[j]); + } + } + + return matrix; +} + +function calculateCovariance(x: number[], y: number[]): number { + if (x.length !== y.length || x.length < 2) return 0; + + const n = x.length; + const meanX = x.reduce((sum, val) => sum + val, 0) / n; + const meanY = y.reduce((sum, val) => sum + val, 0) / n; + + return x.reduce((sum, val, i) => sum + (val - meanX) * (y[i] - meanY), 0) / (n - 1); +} + +// calculatePortfolioVariance is already exported above + +function calculateVarianceGradient(weights: number[], covarianceMatrix: number[][]): number[] { + const n = weights.length; + const gradient: number[] = []; + + for (let i = 0; i < n; i++) { + let grad = 0; + for (let j = 0; j < n; j++) { + grad += 2 * weights[j] * covarianceMatrix[i][j]; + } + gradient[i] = grad; + } + + return gradient; +} + +function findMinimumVarianceWeights( + expectedReturns: number[], + covarianceMatrix: number[][], + targetReturn: number +): number[] | null { + const n = expectedReturns.length; + + // Simplified implementation - in practice would use quadratic programming solver + // Start with equal weights and adjust + const weights = new Array(n).fill(1 / n); + + // Iterative adjustment to meet target return constraint + for (let iter = 0; iter < 50; iter++) { + const currentReturn = weights.reduce((sum, w, i) => sum + w * expectedReturns[i], 0); + const returnDiff = targetReturn - currentReturn; + + if (Math.abs(returnDiff) < 0.001) break; + + // Adjust weights proportionally to expected returns + const totalExpectedReturn = expectedReturns.reduce((sum, r) => sum + Math.abs(r), 0); + + for (let i = 0; i < n; i++) { + const adjustment = (returnDiff * Math.abs(expectedReturns[i])) / totalExpectedReturn; + weights[i] = Math.max(0, weights[i] + adjustment * 0.1); + } + + // Normalize weights + const weightSum = weights.reduce((sum, w) => sum + w, 0); + if (weightSum > 0) { + for (let i = 0; i < n; i++) { + weights[i] /= weightSum; + } + } + } + + return weights; } diff --git a/libs/utils/src/calculations/position-sizing.ts b/libs/utils/src/calculations/position-sizing.ts index cf68c82..e82775f 100644 --- a/libs/utils/src/calculations/position-sizing.ts +++ b/libs/utils/src/calculations/position-sizing.ts @@ -452,3 +452,73 @@ export function validatePositionSize( violations }; } + +/** + * Optimal F position sizing (Ralph Vince's method) + */ +export function optimalFPositionSize( + accountSize: number, + historicalReturns: number[], + maxIterations: number = 100 +): number { + if (historicalReturns.length === 0 || accountSize <= 0) return 0; + + // Convert returns to P&L per unit + const pnlValues = historicalReturns.map(ret => ret * 1000); // Assuming $1000 per unit + + let bestF = 0; + let bestTWR = 0; // Terminal Wealth Relative + + // Test different f values (0.01 to 1.00) + for (let f = 0.01; f <= 1.0; f += 0.01) { + let twr = 1.0; + let valid = true; + + for (const pnl of pnlValues) { + const hpr = 1 + (f * pnl / 1000); // Holding Period Return + + if (hpr <= 0) { + valid = false; + break; + } + + twr *= hpr; + } + + if (valid && twr > bestTWR) { + bestTWR = twr; + bestF = f; + } + } + + // Apply safety factor + const safeF = bestF * 0.75; // 75% of optimal f for safety + + return accountSize * safeF; +} + +/** + * Secure F position sizing (safer version of Optimal F) + */ +export function secureFPositionSize( + accountSize: number, + historicalReturns: number[], + confidenceLevel: number = 0.95 +): number { + if (historicalReturns.length === 0 || accountSize <= 0) return 0; + + // Sort returns to find worst-case scenarios + const sortedReturns = [...historicalReturns].sort((a, b) => a - b); + const worstCaseIndex = Math.floor((1 - confidenceLevel) * sortedReturns.length); + const worstCaseReturn = sortedReturns[worstCaseIndex]; + + // Calculate maximum position size that won't bankrupt at confidence level + const maxLoss = Math.abs(worstCaseReturn); + const maxRiskPercentage = 0.02; // Never risk more than 2% on worst case + + if (maxLoss === 0) return accountSize * 0.1; // Default to 10% if no historical losses + + const secureF = Math.min(maxRiskPercentage / maxLoss, 0.25); // Cap at 25% + + return accountSize * secureF; +} diff --git a/libs/utils/src/calculations/risk-metrics.ts b/libs/utils/src/calculations/risk-metrics.ts index 344ed4e..62da045 100644 --- a/libs/utils/src/calculations/risk-metrics.ts +++ b/libs/utils/src/calculations/risk-metrics.ts @@ -421,3 +421,129 @@ export function riskAdjustedReturn( if (portfolioRisk === 0) return 0; return (portfolioReturn - riskFreeRate) / portfolioRisk; } + +/** + * Calculate Omega Ratio (probability-weighted ratio of gains vs losses) + */ +export function omegaRatio(returns: number[], threshold: number = 0): number { + if (returns.length === 0) return 0; + + let gainsSum = 0; + let lossesSum = 0; + + for (const ret of returns) { + const excessReturn = ret - threshold; + if (excessReturn > 0) { + gainsSum += excessReturn; + } else { + lossesSum += Math.abs(excessReturn); + } + } + + return lossesSum > 0 ? gainsSum / lossesSum : (gainsSum > 0 ? Infinity : 0); +} + +/** + * Calculate Upside Potential Ratio + */ +export function upsidePotentialRatio(returns: number[], threshold: number = 0): number { + if (returns.length === 0) return 0; + + let upsidePotentialSum = 0; + let downsideDeviationSum = 0; + + for (const ret of returns) { + const excessReturn = ret - threshold; + if (excessReturn > 0) { + upsidePotentialSum += excessReturn; + } else { + downsideDeviationSum += excessReturn * excessReturn; + } + } + + const downsideDeviation = Math.sqrt(downsideDeviationSum / returns.length); + const avgUpsidePotential = upsidePotentialSum / returns.length; + + return downsideDeviation > 0 ? avgUpsidePotential / downsideDeviation : 0; +} + +/** + * Calculate maximum drawdown duration + */ +export function drawdownDuration(equityCurve: number[]): { + maxDuration: number; + currentDuration: number; + avgDuration: number; +} { + if (equityCurve.length === 0) { + return { maxDuration: 0, currentDuration: 0, avgDuration: 0 }; + } + + let peak = equityCurve[0]; + let maxDuration = 0; + let currentDuration = 0; + const durations: number[] = []; + let inDrawdown = false; + let drawdownStart = 0; + + for (let i = 1; i < equityCurve.length; i++) { + if (equityCurve[i] > peak) { + if (inDrawdown) { + // End of drawdown + const duration = i - drawdownStart; + durations.push(duration); + maxDuration = Math.max(maxDuration, duration); + inDrawdown = false; + currentDuration = 0; + } + peak = equityCurve[i]; + } else { + if (!inDrawdown) { + // Start of drawdown + inDrawdown = true; + drawdownStart = i; + } + currentDuration = i - drawdownStart; + } + } + + // If still in drawdown at the end + if (inDrawdown) { + maxDuration = Math.max(maxDuration, currentDuration); + } + + const avgDuration = durations.length > 0 ? + durations.reduce((sum, dur) => sum + dur, 0) / durations.length : 0; + + return { + maxDuration, + currentDuration: inDrawdown ? currentDuration : 0, + avgDuration + }; +} + +/** + * Calculate CAPM expected return + * Uses the Capital Asset Pricing Model: E(R) = Rf + β(E(Rm) - Rf) + */ +export function capmExpectedReturn( + riskFreeRate: number, + marketReturn: number, + assetBeta: number +): number { + return riskFreeRate + assetBeta * (marketReturn - riskFreeRate); +} + +/** + * Calculate Jensen's Alpha + * Jensen's Alpha = Portfolio Return - CAPM Expected Return + */ +export function jensenAlpha( + portfolioReturn: number, + riskFreeRate: number, + marketReturn: number, + portfolioBeta: number +): number { + const expectedReturn = capmExpectedReturn(riskFreeRate, marketReturn, portfolioBeta); + return portfolioReturn - expectedReturn; +} diff --git a/libs/utils/src/calculations/technical-indicators.ts b/libs/utils/src/calculations/technical-indicators.ts index eb5cc80..e78526b 100644 --- a/libs/utils/src/calculations/technical-indicators.ts +++ b/libs/utils/src/calculations/technical-indicators.ts @@ -497,3 +497,151 @@ export function aroon(ohlcv: OHLCVData[], period: number = 14): { up: number[], return { up, down }; } + +/** + * Average Directional Movement Index (ADX) and Directional Movement Indicators (DMI) + */ +export function adx(ohlcv: OHLCVData[], period: number = 14): { adx: number[], plusDI: number[], minusDI: number[] } { + if (period >= ohlcv.length) return { adx: [], plusDI: [], minusDI: [] }; + + const trueRanges: number[] = []; + const plusDM: number[] = []; + const minusDM: number[] = []; + + // Calculate True Range and Directional Movements + for (let i = 1; i < ohlcv.length; i++) { + const current = ohlcv[i]; + const previous = ohlcv[i - 1]; + + // True Range + const tr = Math.max( + current.high - current.low, + Math.abs(current.high - previous.close), + Math.abs(current.low - previous.close) + ); + trueRanges.push(tr); + + // Directional Movements + const highDiff = current.high - previous.high; + const lowDiff = previous.low - current.low; + + const plusDMValue = (highDiff > lowDiff && highDiff > 0) ? highDiff : 0; + const minusDMValue = (lowDiff > highDiff && lowDiff > 0) ? lowDiff : 0; + + plusDM.push(plusDMValue); + minusDM.push(minusDMValue); + } + + // Calculate smoothed averages + const atrValues = sma(trueRanges, period); + const smoothedPlusDM = sma(plusDM, period); + const smoothedMinusDM = sma(minusDM, period); + + const plusDI: number[] = []; + const minusDI: number[] = []; + const dx: number[] = []; + + // Calculate DI+ and DI- + for (let i = 0; i < atrValues.length; i++) { + const diPlus = atrValues[i] > 0 ? (smoothedPlusDM[i] / atrValues[i]) * 100 : 0; + const diMinus = atrValues[i] > 0 ? (smoothedMinusDM[i] / atrValues[i]) * 100 : 0; + + plusDI.push(diPlus); + minusDI.push(diMinus); + + // Calculate DX + const diSum = diPlus + diMinus; + const dxValue = diSum > 0 ? (Math.abs(diPlus - diMinus) / diSum) * 100 : 0; + dx.push(dxValue); + } + + // Calculate ADX (smoothed DX) + const adxValues = sma(dx, period); + + return { + adx: adxValues, + plusDI: plusDI.slice(period - 1), + minusDI: minusDI.slice(period - 1) + }; +} + +/** + * Volume Weighted Moving Average (VWMA) + */ +export function vwma(ohlcv: OHLCVData[], period: number = 20): number[] { + if (period >= ohlcv.length) return []; + + const result: number[] = []; + + for (let i = period - 1; i < ohlcv.length; i++) { + const slice = ohlcv.slice(i - period + 1, i + 1); + + let totalVolumePrice = 0; + let totalVolume = 0; + + for (const candle of slice) { + const typicalPrice = (candle.high + candle.low + candle.close) / 3; + totalVolumePrice += typicalPrice * candle.volume; + totalVolume += candle.volume; + } + + const vwmaValue = totalVolume > 0 ? totalVolumePrice / totalVolume : 0; + result.push(vwmaValue); + } + + return result; +} + +/** + * Pivot Points (Standard) + */ +export function pivotPoints(ohlcv: OHLCVData[]): Array<{ + pivot: number; + resistance1: number; + resistance2: number; + resistance3: number; + support1: number; + support2: number; + support3: number; +}> { + if (ohlcv.length === 0) return []; + + const result: Array<{ + pivot: number; + resistance1: number; + resistance2: number; + resistance3: number; + support1: number; + support2: number; + support3: number; + }> = []; + + for (let i = 0; i < ohlcv.length; i++) { + const candle = ohlcv[i]; + + // Calculate pivot point + const pivot = (candle.high + candle.low + candle.close) / 3; + + // Calculate resistance and support levels + const resistance1 = (2 * pivot) - candle.low; + const support1 = (2 * pivot) - candle.high; + + const resistance2 = pivot + (candle.high - candle.low); + const support2 = pivot - (candle.high - candle.low); + + const resistance3 = candle.high + 2 * (pivot - candle.low); + const support3 = candle.low - 2 * (candle.high - pivot); + + result.push({ + pivot, + resistance1, + resistance2, + resistance3, + support1, + support2, + support3 + }); + } + + return result; +} diff --git a/libs/utils/test/simple-test.ts b/libs/utils/test/simple-test.ts deleted file mode 100644 index c0a90b7..0000000 --- a/libs/utils/test/simple-test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { fixedRiskPositionSize } from '../src/calculations/position-sizing.js'; - -try { - console.log('Testing position sizing calculations...'); - - const result = fixedRiskPositionSize({ - accountSize: 100000, - riskPercentage: 2, - entryPrice: 100, - stopLoss: 95 - }); - - console.log('Fixed risk position size result:', result); - console.log('Expected: 400 shares'); - console.log('Test passed:', result === 400); - -} catch (error) { - console.error('Error:', error); -} diff --git a/libs/utils/test/validation.ts b/libs/utils/test/validation.ts deleted file mode 100644 index 45381e9..0000000 --- a/libs/utils/test/validation.ts +++ /dev/null @@ -1,138 +0,0 @@ -/** - * Validation script for position sizing calculations - */ -import { - fixedRiskPositionSize, - kellyPositionSize, - volatilityTargetPositionSize, - equalWeightPositionSize, - atrBasedPositionSize, - expectancyPositionSize, - calculatePortfolioHeat, - validatePositionSize -} from '../src/calculations/position-sizing.js'; - -console.log('=== Position Sizing Calculation Validation ===\n'); - -// Test 1: Fixed Risk Position Sizing -console.log('1. Fixed Risk Position Sizing'); -const fixedRiskResult = fixedRiskPositionSize({ - accountSize: 100000, - riskPercentage: 2, - entryPrice: 100, - stopLoss: 95, - leverage: 1 -}); -console.log(` Account: $100,000, Risk: 2%, Entry: $100, Stop: $95`); -console.log(` Result: ${fixedRiskResult} shares`); -console.log(` Expected: 400 shares (Risk: $2,000 ÷ $5 risk per share = 400)`); -console.log(` ✓ ${fixedRiskResult === 400 ? 'PASS' : 'FAIL'}\n`); - -// Test 2: Kelly Criterion -console.log('2. Kelly Criterion Position Sizing'); -const kellyResult = kellyPositionSize({ - winRate: 0.6, - averageWin: 150, - averageLoss: -100 -}, 100000); -console.log(` Win Rate: 60%, Avg Win: $150, Avg Loss: $100`); -console.log(` Result: $${kellyResult.toFixed(0)}`); -console.log(` Kelly formula with safety factor applied`); -console.log(` ✓ ${kellyResult > 0 && kellyResult < 25000 ? 'PASS' : 'FAIL'}\n`); - -// Test 3: Volatility Target Position Sizing -console.log('3. Volatility Target Position Sizing'); -const volResult = volatilityTargetPositionSize({ - price: 100, - volatility: 0.20, - targetVolatility: 0.10, - lookbackDays: 30 -}, 100000); -console.log(` Price: $100, Asset Vol: 20%, Target Vol: 10%`); -console.log(` Result: ${volResult} shares`); -console.log(` Expected: 500 shares (Vol ratio 0.5 * $100k = $50k ÷ $100 = 500)`); -console.log(` ✓ ${volResult === 500 ? 'PASS' : 'FAIL'}\n`); - -// Test 4: Equal Weight Position Sizing -console.log('4. Equal Weight Position Sizing'); -const equalResult = equalWeightPositionSize(100000, 5, 100); -console.log(` Account: $100,000, Positions: 5, Price: $100`); -console.log(` Result: ${equalResult} shares`); -console.log(` Expected: 200 shares ($100k ÷ 5 = $20k ÷ $100 = 200)`); -console.log(` ✓ ${equalResult === 200 ? 'PASS' : 'FAIL'}\n`); - -// Test 5: ATR-Based Position Sizing -console.log('5. ATR-Based Position Sizing'); -const atrResult = atrBasedPositionSize(100000, 2, 5, 2, 100); -console.log(` Account: $100,000, Risk: 2%, ATR: $5, Multiplier: 2`); -console.log(` Result: ${atrResult} shares`); -console.log(` Expected: 200 shares (Risk: $2k ÷ Stop: $10 = 200)`); -console.log(` ✓ ${atrResult === 200 ? 'PASS' : 'FAIL'}\n`); - -// Test 6: Expectancy Position Sizing -console.log('6. Expectancy Position Sizing'); -const expectancyResult = expectancyPositionSize(100000, 0.6, 150, -100, 5); -console.log(` Win Rate: 60%, Avg Win: $150, Avg Loss: $100`); -console.log(` Result: $${expectancyResult.toFixed(0)}`); -console.log(` Expectancy: 0.6*150 - 0.4*100 = 50 (positive expectancy)`); -console.log(` ✓ ${expectancyResult > 0 ? 'PASS' : 'FAIL'}\n`); - -// Test 7: Portfolio Heat Calculation -console.log('7. Portfolio Heat Calculation'); -const heatResult = calculatePortfolioHeat([ - { value: 10000, risk: 500 }, - { value: 15000, risk: 750 }, - { value: 20000, risk: 1000 } -], 100000); -console.log(` Positions with risks: $500, $750, $1000`); -console.log(` Result: ${heatResult}%`); -console.log(` Expected: 2.25% (Total risk: $2250 ÷ $100k = 2.25%)`); -console.log(` ✓ ${heatResult === 2.25 ? 'PASS' : 'FAIL'}\n`); - -// Test 8: Position Size Validation -console.log('8. Position Size Validation'); -const validationResult = validatePositionSize(50, 100, 100000, 10, 2); -console.log(` Position: 50 shares @ $100, Account: $100k, Max: 10%`); -console.log(` Result: ${validationResult.isValid ? 'Valid' : 'Invalid'}`); -console.log(` Position value: $5,000 (5% of account - within 10% limit)`); -console.log(` ✓ ${validationResult.isValid ? 'PASS' : 'FAIL'}\n`); - -// Test edge cases -console.log('=== Edge Case Testing ===\n'); - -// Zero/negative inputs -console.log('9. Zero/Negative Input Handling'); -const zeroResult = fixedRiskPositionSize({ - accountSize: 0, - riskPercentage: 2, - entryPrice: 100, - stopLoss: 95 -}); -console.log(` Zero account size result: ${zeroResult}`); -console.log(` ✓ ${zeroResult === 0 ? 'PASS' : 'FAIL'}`); - -const equalStopResult = fixedRiskPositionSize({ - accountSize: 100000, - riskPercentage: 2, - entryPrice: 100, - stopLoss: 100 -}); -console.log(` Equal entry/stop result: ${equalStopResult}`); -console.log(` ✓ ${equalStopResult === 0 ? 'PASS' : 'FAIL'}\n`); - -// Negative expectancy Kelly -console.log('10. Negative Expectancy Kelly'); -const negativeKellyResult = kellyPositionSize({ - winRate: 0.3, - averageWin: 100, - averageLoss: -200 -}, 100000); -console.log(` Win Rate: 30%, Avg Win: $100, Avg Loss: $200`); -console.log(` Result: $${negativeKellyResult}`); -console.log(` Expected: $0 (negative expectancy)`); -console.log(` ✓ ${negativeKellyResult === 0 ? 'PASS' : 'FAIL'}\n`); - -console.log('=== Validation Complete ==='); -console.log('All position sizing calculations have been validated!'); -console.log('The functions now include proper input validation, edge case handling,'); -console.log('and mathematically correct implementations.');