UNPKG

quantitivecalc

Version:

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

140 lines (139 loc) 6.12 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.calculateRebalancing = calculateRebalancing; /** * Calculates portfolio rebalancing actions over time based on asset prices, target weights, and a rebalance threshold. * * @param data - Array of records containing asset prices and dates for each period. * @param priceColumns - List of keys in each record that represent asset price columns. * @param targetWeights - Object mapping asset names to their target portfolio weights (e.g., `{ 'AAPL': 0.5, 'GOOG': 0.5 }`). * @param rebalanceThreshold - Maximum allowed drift from target weights before triggering a rebalance (default: `0.05`). * @param portfolioValue - Initial total value of the portfolio (default: `100000`). * @param dateColumn - Key in each record representing the date (default: `'date'`). * @returns Array of rebalancing results for each period, including current weights, drift, rebalance status, and required trades. * * @remarks * - The function assumes that `targetWeights` sum to 1.0. * - Trades are calculated as the number of shares to buy/sell to reach target weights when rebalancing is required. * - If no rebalance is required, trades will be zero for all assets. * * @example * ```typescript * const results = calculateRebalancing( * priceData, * ['AAPL', 'GOOG'], * { AAPL: 0.6, GOOG: 0.4 }, * 0.05, * 50000, * 'date' * ); * ``` */ function calculateRebalancing(data, priceColumns, targetWeights, rebalanceThreshold = 0.05, portfolioValue = 100000, dateColumn = 'date') { if (!data || data.length === 0) { return []; } // Validate that target weights sum to 1 const totalWeight = Object.values(targetWeights).reduce((sum, weight) => sum + weight, 0); if (Math.abs(totalWeight - 1.0) > 0.001) { throw new Error(`Target weights must sum to 1.0, but sum to ${totalWeight}`); } const currentShares = {}; const results = []; // Initialize portfolio with target weights based on first period prices const initialPrices = data[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; } else { currentShares[assetColumn] = 0; console.warn(`Invalid price for ${assetColumn} in initial period: ${price}`); } }); data.forEach((row, index) => { // Calculate current portfolio value let currentPortfolioValue = 0; const currentWeights = {}; // First pass: calculate total portfolio value priceColumns.forEach(assetColumn => { const price = row[assetColumn]; if (typeof price === 'number' && !isNaN(price) && price > 0) { const assetValue = currentShares[assetColumn] * price; currentPortfolioValue += assetValue; } }); // Handle edge case where portfolio value becomes zero if (currentPortfolioValue <= 0) { console.warn(`Portfolio value is zero or negative at period ${index}`); return; } // Second pass: calculate current weights priceColumns.forEach(assetColumn => { const price = row[assetColumn]; if (typeof price === 'number' && !isNaN(price) && price > 0) { const assetValue = currentShares[assetColumn] * price; currentWeights[assetColumn] = assetValue / currentPortfolioValue; } else { currentWeights[assetColumn] = 0; } }); // Calculate drift from target const driftFromTarget = {}; let maxDrift = 0; priceColumns.forEach(assetColumn => { const targetWeight = targetWeights[assetColumn] || 0; const currentWeight = currentWeights[assetColumn] || 0; const drift = Math.abs(currentWeight - targetWeight); driftFromTarget[assetColumn] = drift; maxDrift = Math.max(maxDrift, drift); }); const 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; } else { trades[assetColumn] = 0; } }); // Update current shares after calculating all trades priceColumns.forEach(assetColumn => { currentShares[assetColumn] += trades[assetColumn]; }); } else { // No rebalancing needed - set all trades to zero priceColumns.forEach(assetColumn => { trades[assetColumn] = 0; }); } results.push({ date: typeof row[dateColumn] === 'string' ? row[dateColumn] : row[dateColumn] !== undefined && row[dateColumn] !== null ? String(row[dateColumn]) : `Period_${index}`, currentWeights, targetWeights: { ...targetWeights }, driftFromTarget, rebalanceRequired, trades, }); }); return results; }