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