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
JavaScript
;
/**
* 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;
}