UNPKG

quantitivecalc

Version:

A TypeScript library providing advanced quantitative finance functions for risk analysis, performance metrics, and technical indicators. (Currently in development)

283 lines (282 loc) 12 kB
"use strict"; /** * Portfolio Analysis Utilities * * Functions: * - calculateCorrelationMatrix: Calculates asset correlation analysis between multiple assets * - calculatePortfolioReturns: Calculates weighted returns for multiple assets in a portfolio * - calculateRebalancing: Calculates portfolio weight adjustments over time * - calculateRiskContribution: Calculates each asset's contribution to total portfolio risk * * The functions work with portfolio data containing multiple assets and their weights/returns. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.calculateCorrelationMatrix = calculateCorrelationMatrix; exports.calculatePortfolioReturns = calculatePortfolioReturns; exports.calculateRebalancing = calculateRebalancing; exports.calculateRiskContribution = calculateRiskContribution; function calculateCorrelationMatrix(data, assetColumns, windowSize) { if (!data || data.length === 0 || assetColumns.length === 0) { return []; } const results = []; const effectiveWindowSize = windowSize || data.length; for (let i = effectiveWindowSize - 1; i < data.length; i++) { const windowData = data.slice(i - effectiveWindowSize + 1, i + 1); const correlationMatrix = {}; // Initialize matrix assetColumns.forEach(asset1 => { correlationMatrix[asset1] = {}; assetColumns.forEach(asset2 => { correlationMatrix[asset1][asset2] = asset1 === asset2 ? 1 : 0; }); }); // Calculate correlations for (let j = 0; j < assetColumns.length; j++) { for (let k = j; k < assetColumns.length; k++) { const asset1 = assetColumns[j]; const asset2 = assetColumns[k]; if (asset1 === asset2) { correlationMatrix[asset1][asset2] = 1; continue; } // Extract valid data pairs const pairs = []; windowData.forEach(row => { const val1 = row[asset1]; const val2 = row[asset2]; if (typeof val1 === 'number' && typeof val2 === 'number' && !isNaN(val1) && !isNaN(val2)) { pairs.push([val1, val2]); } }); if (pairs.length < 2) { correlationMatrix[asset1][asset2] = 0; correlationMatrix[asset2][asset1] = 0; continue; } // Calculate means const mean1 = pairs.reduce((sum, pair) => sum + pair[0], 0) / pairs.length; const mean2 = pairs.reduce((sum, pair) => sum + pair[1], 0) / pairs.length; // Calculate correlation let numerator = 0; let sumSq1 = 0; let sumSq2 = 0; pairs.forEach(([val1, val2]) => { const diff1 = val1 - mean1; const diff2 = val2 - mean2; numerator += diff1 * diff2; sumSq1 += diff1 * diff1; sumSq2 += diff2 * diff2; }); const denominator = Math.sqrt(sumSq1 * sumSq2); const correlation = denominator === 0 ? 0 : numerator / denominator; correlationMatrix[asset1][asset2] = correlation; correlationMatrix[asset2][asset1] = correlation; } } results.push(correlationMatrix); } return results; } function calculatePortfolioReturns(data, weights, returnColumns, portfolioReturnColumn = 'portfolio_return') { if (!data || data.length === 0) { return []; } return data.map(row => { let portfolioReturn = 0; let totalWeight = 0; let validAssets = 0; // Calculate weighted return returnColumns.forEach(assetColumn => { const assetReturn = row[assetColumn]; const assetWeight = weights[assetColumn] || 0; if (typeof assetReturn === 'number' && !isNaN(assetReturn) && assetWeight > 0) { portfolioReturn += assetReturn * assetWeight; totalWeight += assetWeight; validAssets++; } }); // Normalize if weights don't sum to 1 if (totalWeight > 0 && totalWeight !== 1) { portfolioReturn = portfolioReturn / totalWeight; } return { ...row, [portfolioReturnColumn]: validAssets > 0 ? portfolioReturn : null, totalWeight, validAssets }; }); } function calculateRebalancing(data, priceColumns, targetWeights, rebalanceThreshold = 0.05, portfolioValue = 100000, dateColumn = 'date') { if (!data || data.length === 0) { return []; } let currentShares = {}; const results = []; // Initialize portfolio with target weights const initialPrices = data[0]; let totalInitialValue = 0; priceColumns.forEach(assetColumn => { const price = initialPrices[assetColumn]; const targetWeight = targetWeights[assetColumn] || 0; if (typeof price === 'number' && !isNaN(price) && price > 0) { const assetValue = portfolioValue * targetWeight; currentShares[assetColumn] = assetValue / price; totalInitialValue += assetValue; } else { currentShares[assetColumn] = 0; } }); data.forEach((row, index) => { // Calculate current portfolio value and weights let currentPortfolioValue = 0; const currentWeights = {}; priceColumns.forEach(assetColumn => { const price = row[assetColumn]; if (typeof price === 'number' && !isNaN(price) && price > 0) { const assetValue = currentShares[assetColumn] * price; currentPortfolioValue += assetValue; } }); // Calculate current weights priceColumns.forEach(assetColumn => { const price = row[assetColumn]; if (typeof price === 'number' && !isNaN(price) && price > 0 && currentPortfolioValue > 0) { const assetValue = currentShares[assetColumn] * price; currentWeights[assetColumn] = assetValue / currentPortfolioValue; } else { currentWeights[assetColumn] = 0; } }); // Calculate drift from target const driftFromTarget = {}; let maxDrift = 0; let rebalanceRequired = false; priceColumns.forEach(assetColumn => { const drift = Math.abs(currentWeights[assetColumn] - (targetWeights[assetColumn] || 0)); driftFromTarget[assetColumn] = drift; maxDrift = Math.max(maxDrift, drift); }); rebalanceRequired = maxDrift > rebalanceThreshold; // Calculate trades needed for rebalancing const trades = {}; if (rebalanceRequired) { priceColumns.forEach(assetColumn => { const price = row[assetColumn]; const targetWeight = targetWeights[assetColumn] || 0; const currentWeight = currentWeights[assetColumn] || 0; if (typeof price === 'number' && !isNaN(price) && price > 0) { const targetValue = currentPortfolioValue * targetWeight; const currentValue = currentPortfolioValue * currentWeight; const tradeValue = targetValue - currentValue; const tradeShares = tradeValue / price; trades[assetColumn] = tradeShares; // Update shares if rebalancing currentShares[assetColumn] += tradeShares; } else { trades[assetColumn] = 0; } }); } else { priceColumns.forEach(assetColumn => { trades[assetColumn] = 0; }); } results.push({ date: row[dateColumn] || `Period_${index}`, currentWeights, targetWeights: { ...targetWeights }, driftFromTarget, rebalanceRequired, trades }); }); return results; } function calculateRiskContribution(returns, weights, returnColumns, windowSize = 252) { if (!returns || returns.length < windowSize || returnColumns.length === 0) { const emptyResult = {}; returnColumns.forEach(asset => { emptyResult[asset] = { volatility: 0, marginalRisk: 0, componentRisk: 0, contributionPercent: 0 }; }); return emptyResult; } // Use the most recent window of data const windowData = returns.slice(-windowSize); // Calculate asset volatilities (standard deviation of returns) const volatilities = {}; const assetReturns = {}; returnColumns.forEach(asset => { const validReturns = []; windowData.forEach(row => { const ret = row[asset]; if (typeof ret === 'number' && !isNaN(ret)) { validReturns.push(ret); } }); assetReturns[asset] = validReturns; if (validReturns.length > 1) { const mean = validReturns.reduce((sum, ret) => sum + ret, 0) / validReturns.length; const variance = validReturns.reduce((sum, ret) => sum + Math.pow(ret - mean, 2), 0) / (validReturns.length - 1); volatilities[asset] = Math.sqrt(variance); } else { volatilities[asset] = 0; } }); // Calculate correlation matrix for the assets const correlationMatrices = calculateCorrelationMatrix(windowData, returnColumns, windowSize); const correlationMatrix = correlationMatrices.length > 0 ? correlationMatrices[correlationMatrices.length - 1] : {}; // Calculate portfolio volatility let portfolioVariance = 0; returnColumns.forEach(asset1 => { returnColumns.forEach(asset2 => { const weight1 = weights[asset1] || 0; const weight2 = weights[asset2] || 0; const vol1 = volatilities[asset1] || 0; const vol2 = volatilities[asset2] || 0; const correlation = correlationMatrix[asset1] ? (correlationMatrix[asset1][asset2] || 0) : 0; portfolioVariance += weight1 * weight2 * vol1 * vol2 * correlation; }); }); const portfolioVolatility = Math.sqrt(Math.max(0, portfolioVariance)); // Calculate risk contributions const riskContribution = {}; returnColumns.forEach(asset => { const assetWeight = weights[asset] || 0; const assetVolatility = volatilities[asset] || 0; // Calculate marginal risk contribution let marginalRisk = 0; if (portfolioVolatility > 0) { returnColumns.forEach(otherAsset => { const otherWeight = weights[otherAsset] || 0; const otherVolatility = volatilities[otherAsset] || 0; const correlation = correlationMatrix[asset] ? (correlationMatrix[asset][otherAsset] || 0) : 0; marginalRisk += otherWeight * otherVolatility * assetVolatility * correlation; }); marginalRisk = marginalRisk / portfolioVolatility; } // Calculate component risk contribution const componentRisk = assetWeight * marginalRisk; // Calculate percentage contribution const contributionPercent = portfolioVolatility > 0 ? (componentRisk / portfolioVolatility) * 100 : 0; riskContribution[asset] = { volatility: assetVolatility, marginalRisk, componentRisk, contributionPercent }; }); return riskContribution; }