582 lines
16 KiB
TypeScript
582 lines
16 KiB
TypeScript
/**
|
|
* 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;
|
|
}
|