/** * Position Sizing Calculations * Risk-based position sizing methods for trading strategies */ export interface PositionSizeParams { accountSize: number; riskPercentage: number; entryPrice: number; stopLoss: number; leverage?: number; } export interface KellyParams { winRate: number; averageWin: number; averageLoss: number; } export interface VolatilityParams { price: number; volatility: number; targetVolatility: number; lookbackDays: number; } /** * Calculate position size based on fixed risk percentage */ export function fixedRiskPositionSize(params: PositionSizeParams): number { const { accountSize, riskPercentage, entryPrice, stopLoss, leverage = 1 } = params; // Input validation if (accountSize <= 0 || riskPercentage <= 0 || entryPrice <= 0 || leverage <= 0) { return 0; } if (entryPrice === stopLoss) { return 0; } const riskAmount = accountSize * (riskPercentage / 100); const riskPerShare = Math.abs(entryPrice - stopLoss); const basePositionSize = riskAmount / riskPerShare; return Math.floor(basePositionSize * leverage); } /** * Calculate position size using Kelly Criterion */ export function kellyPositionSize(params: KellyParams, accountSize: number): number { const { winRate, averageWin, averageLoss } = params; // Validate inputs if (averageLoss === 0 || winRate <= 0 || winRate >= 1 || averageWin <= 0) { return 0; } const lossRate = 1 - winRate; const winLossRatio = averageWin / Math.abs(averageLoss); // Correct Kelly formula: f = (bp - q) / b // where: b = win/loss ratio, p = win rate, q = loss rate const kellyFraction = (winRate * winLossRatio - lossRate) / winLossRatio; // Cap Kelly fraction to prevent over-leveraging (max 25% of Kelly recommendation) const cappedKelly = Math.max(0, Math.min(kellyFraction * 0.25, 0.25)); return accountSize * cappedKelly; } /** * Calculate fractional Kelly position size (more conservative) */ export function fractionalKellyPositionSize( params: KellyParams, accountSize: number, fraction: number = 0.25 ): number { // Input validation if (fraction <= 0 || fraction > 1) { return 0; } const fullKelly = kellyPositionSize(params, accountSize); return fullKelly * fraction; } /** * Calculate position size based on volatility targeting */ export function volatilityTargetPositionSize( params: VolatilityParams, accountSize: number ): number { const { price, volatility, targetVolatility } = params; // Input validation if (volatility <= 0 || price <= 0 || targetVolatility <= 0 || accountSize <= 0) { return 0; } const volatilityRatio = targetVolatility / volatility; const basePositionValue = accountSize * Math.min(volatilityRatio, 2); // Cap at 2x leverage return Math.floor(basePositionValue / price); } /** * Calculate equal weight position size */ export function equalWeightPositionSize( accountSize: number, numberOfPositions: number, price: number ): number { // Input validation if (numberOfPositions <= 0 || price <= 0 || accountSize <= 0) { return 0; } const positionValue = accountSize / numberOfPositions; return Math.floor(positionValue / price); } /** * Calculate position size based on Average True Range (ATR) */ export function atrBasedPositionSize( accountSize: number, riskPercentage: number, atrValue: number, atrMultiplier: number = 2, price: number ): number { if (atrValue === 0 || price === 0) { return 0; } const riskAmount = accountSize * (riskPercentage / 100); const stopDistance = atrValue * atrMultiplier; const positionSize = riskAmount / stopDistance; // Return position size in shares, not dollars return Math.floor(positionSize); } /** * Calculate position size using Van Tharp's expectancy */ export function expectancyPositionSize( accountSize: number, winRate: number, averageWin: number, averageLoss: number, maxRiskPercentage: number = 2 ): number { // Input validation if (accountSize <= 0 || winRate <= 0 || winRate >= 1 || averageWin <= 0 || averageLoss === 0) { return 0; } const expectancy = winRate * averageWin - (1 - winRate) * Math.abs(averageLoss); if (expectancy <= 0) { return 0; } // Scale position size based on expectancy relative to average loss // Higher expectancy relative to risk allows for larger position const expectancyRatio = expectancy / Math.abs(averageLoss); const riskPercentage = Math.min(expectancyRatio * 0.5, maxRiskPercentage); const positionValue = accountSize * (riskPercentage / 100); return positionValue; } /** * Calculate optimal position size using Monte Carlo simulation */ export function monteCarloPositionSize( accountSize: number, historicalReturns: number[], simulations: number = 1000, confidenceLevel: number = 0.95 ): number { if (historicalReturns.length === 0) { return 0; } const outcomes: number[] = []; const mean = historicalReturns.reduce((sum, ret) => sum + ret, 0) / historicalReturns.length; const variance = historicalReturns.reduce((sum, ret) => sum + Math.pow(ret - mean, 2), 0) / historicalReturns.length; const stdDev = Math.sqrt(variance); // Test different position sizes (as fraction of account) const testFractions = [0.01, 0.025, 0.05, 0.075, 0.1, 0.15, 0.2, 0.25]; let optimalFraction = 0; let bestSharpe = -Infinity; for (const fraction of testFractions) { const simOutcomes: number[] = []; for (let i = 0; i < simulations; i++) { let portfolioValue = accountSize; // Simulate trades over a period for (let j = 0; j < 50; j++) { // 50 trades const randomReturn = historicalReturns[Math.floor(Math.random() * historicalReturns.length)]; const positionReturn = randomReturn * fraction; portfolioValue = portfolioValue * (1 + positionReturn); } simOutcomes.push(portfolioValue); } // Calculate Sharpe ratio for this fraction const avgOutcome = simOutcomes.reduce((sum, val) => sum + val, 0) / simOutcomes.length; const returns = simOutcomes.map(val => (val - accountSize) / accountSize); const avgReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length; const returnStdDev = Math.sqrt( returns.reduce((sum, ret) => sum + Math.pow(ret - avgReturn, 2), 0) / returns.length ); const sharpe = returnStdDev > 0 ? avgReturn / returnStdDev : -Infinity; if (sharpe > bestSharpe) { bestSharpe = sharpe; optimalFraction = fraction; } } return accountSize * optimalFraction; } /** * Calculate position size based on Sharpe ratio optimization */ export function sharpeOptimizedPositionSize( accountSize: number, expectedReturn: number, volatility: number, riskFreeRate: number = 0.02, maxLeverage: number = 3 ): number { // Input validation if (volatility <= 0 || accountSize <= 0 || expectedReturn <= riskFreeRate || maxLeverage <= 0) { return 0; } // Kelly criterion with Sharpe ratio optimization const excessReturn = expectedReturn - riskFreeRate; const kellyFraction = excessReturn / (volatility * volatility); // Apply maximum leverage constraint const constrainedFraction = Math.max(0, Math.min(kellyFraction, maxLeverage)); return accountSize * constrainedFraction; } /** * Fixed fractional position sizing */ export function fixedFractionalPositionSize( accountSize: number, riskPercentage: number, stopLossPercentage: number, price: number ): number { // Input validation if (stopLossPercentage <= 0 || price <= 0 || riskPercentage <= 0 || accountSize <= 0) { return 0; } const riskAmount = accountSize * (riskPercentage / 100); const stopLossAmount = price * (stopLossPercentage / 100); return Math.floor(riskAmount / stopLossAmount); } /** * Volatility-adjusted position sizing */ export function volatilityAdjustedPositionSize( accountSize: number, targetVolatility: number, assetVolatility: number, price: number ): number { // Input validation if (assetVolatility <= 0 || price <= 0 || targetVolatility <= 0 || accountSize <= 0) { return 0; } const volatilityRatio = targetVolatility / assetVolatility; const cappedRatio = Math.min(volatilityRatio, 3); // Cap at 3x leverage const positionValue = accountSize * cappedRatio; return Math.floor(positionValue / price); } /** * Calculate position size with correlation adjustment */ export function correlationAdjustedPositionSize( basePositionSize: number, existingPositions: Array<{ size: number; correlation: number }>, maxCorrelationRisk: number = 0.3 ): number { if (existingPositions.length === 0 || basePositionSize <= 0) { return basePositionSize; } // Calculate portfolio correlation risk // This should consider the correlation between the new position and existing ones const totalCorrelationRisk = existingPositions.reduce((total, position) => { // Weight correlation by position size relative to new position const relativeSize = position.size / (basePositionSize + position.size); return total + relativeSize * Math.abs(position.correlation); }, 0); // Adjust position size based on correlation risk const correlationAdjustment = Math.max(0.1, 1 - totalCorrelationRisk / maxCorrelationRisk); return Math.floor(basePositionSize * correlationAdjustment); } /** * Calculate portfolio heat (total risk across all positions) */ export function calculatePortfolioHeat( positions: Array<{ value: number; risk: number }>, accountSize: number ): number { // Input validation if (accountSize <= 0 || positions.length === 0) { return 0; } const totalRisk = positions.reduce((sum, position) => { // Ensure risk values are positive return sum + Math.max(0, position.risk); }, 0); return Math.min((totalRisk / accountSize) * 100, 100); // Cap at 100% } /** * Dynamic position sizing based on market conditions */ export function dynamicPositionSize( basePositionSize: number, marketVolatility: number, normalVolatility: number, drawdownLevel: number, maxDrawdownThreshold: number = 0.1 ): number { // Input validation if (basePositionSize <= 0 || marketVolatility <= 0 || normalVolatility <= 0) { return 0; } if (drawdownLevel < 0 || maxDrawdownThreshold <= 0) { return basePositionSize; } // Volatility adjustment - reduce size when volatility is high const volatilityAdjustment = Math.min(normalVolatility / marketVolatility, 2); // Cap at 2x // Drawdown adjustment - reduce size as drawdown increases const normalizedDrawdown = Math.min(drawdownLevel / maxDrawdownThreshold, 1); const drawdownAdjustment = Math.max(0.1, 1 - normalizedDrawdown); const adjustedSize = basePositionSize * volatilityAdjustment * drawdownAdjustment; return Math.floor(Math.max(0, adjustedSize)); } /** * Calculate maximum position size based on liquidity */ export function liquidityConstrainedPositionSize( desiredPositionSize: number, averageDailyVolume: number, maxVolumePercentage: number = 0.05, price: number ): number { if (averageDailyVolume === 0 || price === 0) { return 0; } const maxShares = averageDailyVolume * maxVolumePercentage; return Math.min(desiredPositionSize, maxShares); } /** * Multi-timeframe position sizing */ export function multiTimeframePositionSize( accountSize: number, shortTermSignal: number, // -1 to 1 mediumTermSignal: number, // -1 to 1 longTermSignal: number, // -1 to 1 baseRiskPercentage: number = 1 ): number { // Input validation if (accountSize <= 0 || baseRiskPercentage <= 0) { return 0; } // Clamp signals to valid range const clampedShort = Math.max(-1, Math.min(1, shortTermSignal)); const clampedMedium = Math.max(-1, Math.min(1, mediumTermSignal)); const clampedLong = Math.max(-1, Math.min(1, longTermSignal)); // Weight the signals (long-term gets higher weight) const weightedSignal = clampedShort * 0.2 + clampedMedium * 0.3 + clampedLong * 0.5; // Adjust risk based on signal strength const adjustedRisk = baseRiskPercentage * Math.abs(weightedSignal); return accountSize * (adjustedRisk / 100); } /** * Risk parity position sizing */ export function riskParityPositionSize( assets: Array<{ volatility: number; price: number }>, targetRisk: number, accountSize: number ): number[] { if (assets.length === 0) { return []; } // Calculate inverse volatility weights const totalInverseVol = assets.reduce((sum, asset) => { if (asset.volatility === 0) { return sum; } return sum + 1 / asset.volatility; }, 0); if (totalInverseVol === 0) { return assets.map(() => 0); } return assets.map(asset => { if (asset.volatility === 0 || asset.price === 0) { return 0; } // Calculate weight based on inverse volatility const weight = 1 / asset.volatility / totalInverseVol; // The weight itself already accounts for risk parity // We just need to scale by target risk once const positionValue = accountSize * weight * targetRisk; return Math.floor(positionValue / asset.price); }); } /** * Validate position size against risk limits */ export function validatePositionSize( positionSize: number, price: number, accountSize: number, maxPositionPercentage: number = 10, maxLeverage: number = 1 ): { isValid: boolean; adjustedSize: number; violations: string[] } { const violations: string[] = []; let adjustedSize = positionSize; // Check maximum position percentage const positionValue = positionSize * price; const positionPercentage = (positionValue / accountSize) * 100; if (positionPercentage > maxPositionPercentage) { violations.push(`Position exceeds maximum ${maxPositionPercentage}% of account`); adjustedSize = (accountSize * maxPositionPercentage) / 100 / price; } // Check leverage limits const leverage = positionValue / accountSize; if (leverage > maxLeverage) { violations.push(`Position exceeds maximum leverage of ${maxLeverage}x`); adjustedSize = Math.min(adjustedSize, (accountSize * maxLeverage) / price); } // Check minimum position size if (adjustedSize < 1 && adjustedSize > 0) { violations.push('Position size too small (less than 1 share)'); adjustedSize = 0; } return { isValid: violations.length === 0, adjustedSize: Math.max(0, adjustedSize), 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; }