/** * Portfolio Analytics * Advanced portfolio analysis and optimization tools */ import { OHLCVData, PriceData } from './index'; export interface PortfolioPosition { symbol: string; shares: number; price: number; value: number; weight: number; } export interface PortfolioAnalysis { totalValue: number; totalReturn: number; volatility: number; sharpeRatio: number; maxDrawdown: number; var95: number; beta: number; alpha: number; treynorRatio: number; informationRatio: number; trackingError: number; } export interface AssetAllocation { symbol: string; targetWeight: number; currentWeight: number; difference: number; rebalanceAmount: number; } export interface PortfolioOptimizationResult { weights: number[]; expectedReturn: number; volatility: number; sharpeRatio: number; symbols: string[]; } /** * Calculate portfolio value and weights */ export function calculatePortfolioMetrics(positions: PortfolioPosition[]): { totalValue: number; weights: number[]; concentrationRisk: number; } { const totalValue = positions.reduce((sum, pos) => sum + pos.value, 0); const weights = positions.map(pos => pos.value / totalValue); // Calculate Herfindahl-Hirschman Index for concentration risk const concentrationRisk = weights.reduce((sum, weight) => sum + weight * weight, 0); return { totalValue, weights, concentrationRisk, }; } /** * Calculate portfolio returns from position returns */ export function calculatePortfolioReturns(assetReturns: number[][], weights: number[]): number[] { if (assetReturns.length === 0 || weights.length !== assetReturns[0].length) { return []; } const portfolioReturns: number[] = []; for (let i = 0; i < assetReturns.length; i++) { let portfolioReturn = 0; for (let j = 0; j < weights.length; j++) { portfolioReturn += weights[j] * assetReturns[i][j]; } portfolioReturns.push(portfolioReturn); } return portfolioReturns; } /** * Mean-Variance Optimization (Markowitz) */ export function markowitzOptimization( expectedReturns: number[], covarianceMatrix: number[][], riskFreeRate: number = 0.02, riskAversion: number = 1 ): PortfolioOptimizationResult { const n = expectedReturns.length; // Simplified optimization using equal weights as baseline // In production, use proper quadratic programming solver const weights = new Array(n).fill(1 / n); const expectedReturn = weights.reduce((sum, weight, i) => sum + weight * expectedReturns[i], 0); // Calculate portfolio variance let portfolioVariance = 0; for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) { portfolioVariance += weights[i] * weights[j] * covarianceMatrix[i][j]; } } const volatility = Math.sqrt(portfolioVariance); const sharpeRatio = volatility > 0 ? (expectedReturn - riskFreeRate) / volatility : 0; return { weights, expectedReturn, volatility, sharpeRatio, symbols: [], // Would be filled with actual symbols }; } /** * Black-Litterman Model */ export function blackLittermanOptimization( marketCaps: number[], covarianceMatrix: number[][], views: Array<{ assets: number[]; expectedReturn: number; confidence: number }>, riskAversion: number = 3, riskFreeRate: number = 0.02 ): PortfolioOptimizationResult { const n = marketCaps.length; // Calculate market weights const totalMarketCap = marketCaps.reduce((sum, cap) => sum + cap, 0); const marketWeights = marketCaps.map(cap => cap / totalMarketCap); // Implied equilibrium returns const equilibriumReturns: number[] = []; for (let i = 0; i < n; i++) { let equilibriumReturn = 0; for (let j = 0; j < n; j++) { equilibriumReturn += riskAversion * covarianceMatrix[i][j] * marketWeights[j]; } equilibriumReturns.push(equilibriumReturn); } // Simplified BL implementation - in production use proper matrix operations const weights = [...marketWeights]; // Start with market weights const expectedReturn = weights.reduce( (sum, weight, i) => sum + weight * equilibriumReturns[i], 0 ); let portfolioVariance = 0; for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) { portfolioVariance += weights[i] * weights[j] * covarianceMatrix[i][j]; } } const volatility = Math.sqrt(portfolioVariance); const sharpeRatio = volatility > 0 ? (expectedReturn - riskFreeRate) / volatility : 0; return { weights, expectedReturn, volatility, sharpeRatio, symbols: [], }; } /** * Risk Parity Portfolio */ export function riskParityOptimization(covarianceMatrix: number[][]): PortfolioOptimizationResult { const n = covarianceMatrix.length; // Start with equal weights let weights = new Array(n).fill(1 / n); // Iterative optimization for equal risk contribution const maxIterations = 100; const tolerance = 1e-8; for (let iter = 0; iter < maxIterations; iter++) { const riskContributions = calculateRiskContributions(weights, covarianceMatrix); const totalRisk = Math.sqrt(calculatePortfolioVariance(weights, covarianceMatrix)); const targetRiskContribution = totalRisk / n; let converged = true; const newWeights = [...weights]; for (let i = 0; i < n; i++) { const diff = riskContributions[i] - targetRiskContribution; if (Math.abs(diff) > tolerance) { converged = false; // Simple adjustment - in production use proper optimization newWeights[i] *= 1 - (diff / totalRisk) * 0.1; } } // Normalize weights const sum = newWeights.reduce((s, w) => s + w, 0); weights = newWeights.map(w => w / sum); if (converged) {break;} } const portfolioVariance = calculatePortfolioVariance(weights, covarianceMatrix); const volatility = Math.sqrt(portfolioVariance); return { weights, expectedReturn: 0, // Not calculated for risk parity volatility, sharpeRatio: 0, symbols: [], }; } /** * Calculate risk contributions for each asset */ export function calculateRiskContributions( weights: number[], covarianceMatrix: number[][] ): number[] { const n = weights.length; const riskContributions: number[] = []; const portfolioVariance = calculatePortfolioVariance(weights, covarianceMatrix); const portfolioVolatility = Math.sqrt(portfolioVariance); for (let i = 0; i < n; i++) { let marginalContribution = 0; for (let j = 0; j < n; j++) { marginalContribution += weights[j] * covarianceMatrix[i][j]; } const riskContribution = (weights[i] * marginalContribution) / portfolioVolatility; riskContributions.push(riskContribution); } return riskContributions; } /** * Calculate portfolio variance */ export function calculatePortfolioVariance( weights: number[], covarianceMatrix: number[][] ): number { const n = weights.length; let variance = 0; for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) { variance += weights[i] * weights[j] * covarianceMatrix[i][j]; } } return variance; } /** * Portfolio rebalancing analysis */ export function calculateRebalancing( currentPositions: PortfolioPosition[], targetWeights: number[], totalValue: number ): AssetAllocation[] { if (currentPositions.length !== targetWeights.length) { throw new Error('Number of positions must match number of target weights'); } return currentPositions.map((position, index) => { const currentWeight = position.value / totalValue; const targetWeight = targetWeights[index]; const difference = targetWeight - currentWeight; const rebalanceAmount = difference * totalValue; return { symbol: position.symbol, targetWeight, currentWeight, difference, rebalanceAmount, }; }); } /** * Factor model analysis (Fama-French) */ export function famaFrenchAnalysis( portfolioReturns: number[], marketReturns: number[], smbReturns: number[], // Small minus Big hmlReturns: number[], // High minus Low riskFreeRate: number = 0.02 ): { alpha: number; marketBeta: number; sizeBeta: number; valueBeta: number; rSquared: number; } { const n = portfolioReturns.length; // Excess returns const excessPortfolioReturns = portfolioReturns.map(r => r - riskFreeRate); const excessMarketReturns = marketReturns.map(r => r - riskFreeRate); // Simple linear regression (in production, use proper multiple regression) const meanExcessPortfolio = excessPortfolioReturns.reduce((sum, r) => sum + r, 0) / n; const meanExcessMarket = excessMarketReturns.reduce((sum, r) => sum + r, 0) / n; const meanSMB = smbReturns.reduce((sum, r) => sum + r, 0) / n; const meanHML = hmlReturns.reduce((sum, r) => sum + r, 0) / n; // Calculate market beta let covariance = 0; let marketVariance = 0; for (let i = 0; i < n; i++) { const portfolioDiff = excessPortfolioReturns[i] - meanExcessPortfolio; const marketDiff = excessMarketReturns[i] - meanExcessMarket; covariance += portfolioDiff * marketDiff; marketVariance += marketDiff * marketDiff; } const marketBeta = marketVariance > 0 ? covariance / marketVariance : 0; const alpha = meanExcessPortfolio - marketBeta * meanExcessMarket; return { alpha, marketBeta, sizeBeta: 0, // Simplified - would need proper regression valueBeta: 0, // Simplified - would need proper regression rSquared: 0, // Simplified - would need proper regression }; } /** * Portfolio performance attribution */ export function performanceAttribution( portfolioReturns: number[], benchmarkReturns: number[], sectorWeights: number[][], sectorReturns: number[][] ): { totalActiveReturn: number; allocationEffect: number; selectionEffect: number; interactionEffect: number; } { const n = portfolioReturns.length; const portfolioReturn = portfolioReturns.reduce((sum, r) => sum + r, 0) / n; const benchmarkReturn = benchmarkReturns.reduce((sum, r) => sum + r, 0) / n; const totalActiveReturn = portfolioReturn - benchmarkReturn; // Simplified attribution analysis let allocationEffect = 0; let selectionEffect = 0; let interactionEffect = 0; // This would require proper implementation with sector-level analysis // For now, return the total active return distributed equally allocationEffect = totalActiveReturn * 0.4; selectionEffect = totalActiveReturn * 0.4; interactionEffect = totalActiveReturn * 0.2; return { totalActiveReturn, allocationEffect, selectionEffect, interactionEffect, }; } /** * Calculate Efficient Frontier points */ export function calculateEfficientFrontier( 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); for (let i = 0; i < numPoints; i++) { const targetReturn = minReturn + i * returnStep; // Find minimum variance portfolio for target return using quadratic programming (simplified) const weights = findMinimumVarianceWeights(expectedReturns, covarianceMatrix, targetReturn); 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 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; }