stock-bot/libs/utils/src/calculations/performance-metrics.ts

789 lines
25 KiB
TypeScript

/**
* Performance Metrics and Analysis
* Comprehensive performance measurement tools for trading strategies and portfolios
*/
import { PortfolioMetrics } from './index';
export interface TradePerformance {
totalTrades: number;
winningTrades: number;
losingTrades: number;
winRate: number;
averageWin: number;
averageLoss: number;
largestWin: number;
largestLoss: number;
profitFactor: number;
expectancy: number;
averageTradeReturn: number;
consecutiveWins: number;
consecutiveLosses: number;
}
export interface DrawdownAnalysis {
maxDrawdown: number;
maxDrawdownDuration: number;
averageDrawdown: number;
drawdownPeriods: Array<{
start: Date;
end: Date;
duration: number;
magnitude: number;
}>;
}
export interface ReturnAnalysis {
totalReturn: number;
annualizedReturn: number;
compoundAnnualGrowthRate: number;
volatility: number;
annualizedVolatility: number;
skewness: number;
kurtosis: number;
bestMonth: number;
worstMonth: number;
positiveMonths: number;
negativeMonths: number;
}
/**
* Calculate comprehensive trade performance metrics
*/
export function analyzeTradePerformance(trades: Array<{ pnl: number; date: Date }>): TradePerformance {
if (trades.length === 0) {
return {
totalTrades: 0,
winningTrades: 0,
losingTrades: 0,
winRate: 0,
averageWin: 0,
averageLoss: 0,
largestWin: 0,
largestLoss: 0,
profitFactor: 0,
expectancy: 0,
averageTradeReturn: 0,
consecutiveWins: 0,
consecutiveLosses: 0
};
}
const winningTrades = trades.filter(trade => trade.pnl > 0);
const losingTrades = trades.filter(trade => trade.pnl < 0);
const totalWins = winningTrades.reduce((sum, trade) => sum + trade.pnl, 0);
const totalLosses = Math.abs(losingTrades.reduce((sum, trade) => sum + trade.pnl, 0));
const averageWin = winningTrades.length > 0 ? totalWins / winningTrades.length : 0;
const averageLoss = losingTrades.length > 0 ? totalLosses / losingTrades.length : 0;
const largestWin = winningTrades.length > 0 ? Math.max(...winningTrades.map(t => t.pnl)) : 0;
const largestLoss = losingTrades.length > 0 ? Math.min(...losingTrades.map(t => t.pnl)) : 0;
const profitFactor = totalLosses > 0 ? totalWins / totalLosses : totalWins > 0 ? Infinity : 0;
const winRate = winningTrades.length / trades.length;
const expectancy = (winRate * averageWin) - ((1 - winRate) * averageLoss);
const totalPnL = trades.reduce((sum, trade) => sum + trade.pnl, 0);
const averageTradeReturn = totalPnL / trades.length;
// Calculate consecutive wins/losses
let consecutiveWins = 0;
let consecutiveLosses = 0;
let currentWinStreak = 0;
let currentLossStreak = 0;
for (const trade of trades) {
if (trade.pnl > 0) {
currentWinStreak++;
currentLossStreak = 0;
consecutiveWins = Math.max(consecutiveWins, currentWinStreak);
} else if (trade.pnl < 0) {
currentLossStreak++;
currentWinStreak = 0;
consecutiveLosses = Math.max(consecutiveLosses, currentLossStreak);
}
}
return {
totalTrades: trades.length,
winningTrades: winningTrades.length,
losingTrades: losingTrades.length,
winRate,
averageWin,
averageLoss,
largestWin,
largestLoss,
profitFactor,
expectancy,
averageTradeReturn,
consecutiveWins,
consecutiveLosses
};
}
/**
* Analyze drawdown characteristics
*/
export function analyzeDrawdowns(equityCurve: Array<{ value: number; date: Date }>): DrawdownAnalysis {
if (equityCurve.length < 2) {
return {
maxDrawdown: 0,
maxDrawdownDuration: 0,
averageDrawdown: 0,
drawdownPeriods: []
};
}
let peak = equityCurve[0].value;
let peakDate = equityCurve[0].date;
let maxDrawdown = 0;
let maxDrawdownDuration = 0;
const drawdownPeriods: Array<{
start: Date;
end: Date;
duration: number;
magnitude: number;
}> = [];
let currentDrawdownStart: Date | null = null;
let drawdowns: number[] = [];
for (let i = 1; i < equityCurve.length; i++) {
const current = equityCurve[i];
if (current.value > peak) {
// New peak - end any current drawdown
if (currentDrawdownStart) {
const drawdownMagnitude = (peak - equityCurve[i - 1].value) / peak;
const duration = Math.floor((equityCurve[i - 1].date.getTime() - currentDrawdownStart.getTime()) / (1000 * 60 * 60 * 24));
drawdownPeriods.push({
start: currentDrawdownStart,
end: equityCurve[i - 1].date,
duration,
magnitude: drawdownMagnitude
});
drawdowns.push(drawdownMagnitude);
maxDrawdownDuration = Math.max(maxDrawdownDuration, duration);
currentDrawdownStart = null;
}
peak = current.value;
peakDate = current.date;
} else {
// In drawdown
if (!currentDrawdownStart) {
currentDrawdownStart = peakDate;
}
const drawdown = (peak - current.value) / peak;
maxDrawdown = Math.max(maxDrawdown, drawdown);
}
}
// Handle ongoing drawdown
if (currentDrawdownStart) {
const lastPoint = equityCurve[equityCurve.length - 1];
const drawdownMagnitude = (peak - lastPoint.value) / peak;
const duration = Math.floor((lastPoint.date.getTime() - currentDrawdownStart.getTime()) / (1000 * 60 * 60 * 24));
drawdownPeriods.push({
start: currentDrawdownStart,
end: lastPoint.date,
duration,
magnitude: drawdownMagnitude
});
drawdowns.push(drawdownMagnitude);
maxDrawdownDuration = Math.max(maxDrawdownDuration, duration);
}
const averageDrawdown = drawdowns.length > 0 ? drawdowns.reduce((sum, dd) => sum + dd, 0) / drawdowns.length : 0;
return {
maxDrawdown,
maxDrawdownDuration,
averageDrawdown,
drawdownPeriods
};
}
/**
* Analyze return characteristics
*/
export function analyzeReturns(
returns: Array<{ return: number; date: Date }>,
periodsPerYear: number = 252
): ReturnAnalysis {
if (returns.length === 0) {
return {
totalReturn: 0,
annualizedReturn: 0,
compoundAnnualGrowthRate: 0,
volatility: 0,
annualizedVolatility: 0,
skewness: 0,
kurtosis: 0,
bestMonth: 0,
worstMonth: 0,
positiveMonths: 0,
negativeMonths: 0
};
}
const returnValues = returns.map(r => r.return);
// Calculate basic statistics
const totalReturn = returnValues.reduce((product, ret) => product * (1 + ret), 1) - 1;
const averageReturn = returnValues.reduce((sum, ret) => sum + ret, 0) / returnValues.length;
const annualizedReturn = Math.pow(1 + averageReturn, periodsPerYear) - 1;
// Calculate CAGR
const years = returns.length / periodsPerYear;
const cagr = years > 0 ? Math.pow(1 + totalReturn, 1 / years) - 1 : 0;
// Calculate volatility
const variance = returnValues.reduce((sum, ret) => sum + Math.pow(ret - averageReturn, 2), 0) / (returnValues.length - 1);
const volatility = Math.sqrt(variance);
const annualizedVolatility = volatility * Math.sqrt(periodsPerYear);
// Calculate skewness and kurtosis
const skewness = calculateSkewness(returnValues);
const kurtosis = calculateKurtosis(returnValues);
// Monthly analysis
const monthlyReturns = aggregateMonthlyReturns(returns);
const bestMonth = monthlyReturns.length > 0 ? Math.max(...monthlyReturns) : 0;
const worstMonth = monthlyReturns.length > 0 ? Math.min(...monthlyReturns) : 0;
const positiveMonths = monthlyReturns.filter(ret => ret > 0).length;
const negativeMonths = monthlyReturns.filter(ret => ret < 0).length;
return {
totalReturn,
annualizedReturn,
compoundAnnualGrowthRate: cagr,
volatility,
annualizedVolatility,
skewness,
kurtosis,
bestMonth,
worstMonth,
positiveMonths,
negativeMonths
};
}
/**
* Calculate rolling performance metrics
*/
export function calculateRollingMetrics(
returns: number[],
windowSize: number,
metricType: 'sharpe' | 'volatility' | 'return' = 'sharpe'
): number[] {
if (returns.length < windowSize) return [];
const rollingMetrics: number[] = [];
for (let i = windowSize - 1; i < returns.length; i++) {
const window = returns.slice(i - windowSize + 1, i + 1);
switch (metricType) {
case 'sharpe':
rollingMetrics.push(calculateSharpeRatio(window));
break;
case 'volatility':
rollingMetrics.push(calculateVolatility(window));
break;
case 'return':
const avgReturn = window.reduce((sum, ret) => sum + ret, 0) / window.length;
rollingMetrics.push(avgReturn);
break;
}
}
return rollingMetrics;
}
/**
* Calculate performance attribution
*/
export function strategyPerformanceAttribution(
portfolioReturns: number[],
benchmarkReturns: number[],
sectorWeights: number[],
sectorReturns: number[]
): {
allocationEffect: number;
selectionEffect: number;
interactionEffect: number;
totalActiveReturn: number;
} {
if (portfolioReturns.length !== benchmarkReturns.length) {
throw new Error('Portfolio and benchmark returns must have same length');
}
const portfolioReturn = portfolioReturns.reduce((sum, ret) => sum + ret, 0) / portfolioReturns.length;
const benchmarkReturn = benchmarkReturns.reduce((sum, ret) => sum + ret, 0) / benchmarkReturns.length;
let allocationEffect = 0;
let selectionEffect = 0;
let interactionEffect = 0;
for (let i = 0; i < sectorWeights.length; i++) {
const portfolioWeight = sectorWeights[i];
const benchmarkWeight = 1 / sectorWeights.length; // Assuming equal benchmark weights
const sectorReturn = sectorReturns[i];
// Allocation effect: (portfolio weight - benchmark weight) * (benchmark sector return - benchmark return)
allocationEffect += (portfolioWeight - benchmarkWeight) * (sectorReturn - benchmarkReturn);
// Selection effect: benchmark weight * (portfolio sector return - benchmark sector return)
selectionEffect += benchmarkWeight * (sectorReturn - sectorReturn); // Simplified
// Interaction effect: (portfolio weight - benchmark weight) * (portfolio sector return - benchmark sector return)
interactionEffect += (portfolioWeight - benchmarkWeight) * (sectorReturn - sectorReturn); // Simplified
}
const totalActiveReturn = portfolioReturn - benchmarkReturn;
return {
allocationEffect,
selectionEffect,
interactionEffect,
totalActiveReturn
};
}
/**
* Calculate Omega ratio
*/
export function omegaRatio(returns: number[], threshold: number = 0): number {
if (returns.length === 0) return 0;
const gains = returns.filter(ret => ret > threshold).reduce((sum, ret) => sum + (ret - threshold), 0);
const losses = returns.filter(ret => ret < threshold).reduce((sum, ret) => sum + Math.abs(ret - threshold), 0);
return losses === 0 ? Infinity : gains / losses;
}
/**
* Calculate gain-to-pain ratio
*/
export function gainToPainRatio(returns: number[]): number {
if (returns.length === 0) return 0;
const totalGain = returns.reduce((sum, ret) => sum + ret, 0);
const totalPain = returns.filter(ret => ret < 0).reduce((sum, ret) => sum + Math.abs(ret), 0);
return totalPain === 0 ? (totalGain > 0 ? Infinity : 0) : totalGain / totalPain;
}
/**
* Calculate Martin ratio (modified Sharpe with downside deviation)
*/
export function martinRatio(returns: number[], riskFreeRate: number = 0): number {
if (returns.length === 0) return 0;
const averageReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
const downsideReturns = returns.filter(ret => ret < riskFreeRate);
if (downsideReturns.length === 0) return Infinity;
const downsideDeviation = Math.sqrt(
downsideReturns.reduce((sum, ret) => sum + Math.pow(ret - riskFreeRate, 2), 0) / returns.length
);
return downsideDeviation === 0 ? Infinity : (averageReturn - riskFreeRate) / downsideDeviation;
}
/**
* Calculate comprehensive portfolio metrics
*/
export function calculateStrategyMetrics(
equityCurve: Array<{ value: number; date: Date }>,
benchmarkReturns?: number[],
riskFreeRate: number = 0.02
): PortfolioMetrics {
if (equityCurve.length < 2) {
return {
totalValue: 0,
totalReturn: 0,
totalReturnPercent: 0,
dailyReturn: 0,
dailyReturnPercent: 0,
maxDrawdown: 0,
sharpeRatio: 0,
beta: 0,
alpha: 0,
volatility: 0
};
}
const returns = [];
for (let i = 1; i < equityCurve.length; i++) {
const ret = (equityCurve[i].value - equityCurve[i - 1].value) / equityCurve[i - 1].value;
returns.push(ret);
}
const totalValue = equityCurve[equityCurve.length - 1].value;
const totalReturn = totalValue - equityCurve[0].value;
const totalReturnPercent = (totalReturn / equityCurve[0].value) * 100;
const dailyReturn = returns[returns.length - 1];
const dailyReturnPercent = dailyReturn * 100;
const maxDrawdown = analyzeDrawdowns(equityCurve).maxDrawdown;
const sharpeRatio = calculateSharpeRatio(returns, riskFreeRate);
const volatility = calculateVolatility(returns);
let beta = 0;
let alpha = 0;
if (benchmarkReturns && benchmarkReturns.length === returns.length) {
beta = calculateBeta(returns, benchmarkReturns);
alpha = calculateAlpha(returns, benchmarkReturns, riskFreeRate);
}
return {
totalValue,
totalReturn,
totalReturnPercent,
dailyReturn,
dailyReturnPercent,
maxDrawdown,
sharpeRatio,
beta,
alpha,
volatility
};
}
/**
* Calculate Calmar Ratio
*/
export function calmarRatio(returns: number[], equityCurve: Array<{ value: number; date: Date }>, riskFreeRate: number = 0): number {
const maxDrawdown = analyzeDrawdowns(equityCurve).maxDrawdown;
const avgReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
return maxDrawdown === 0 ? 0 : (avgReturn - riskFreeRate) / maxDrawdown;
}
/**
* Calculate Sterling Ratio
*/
export function sterlingRatio(returns: number[], equityCurve: Array<{ value: number; date: Date }>, riskFreeRate: number = 0): number {
const averageDrawdown = analyzeDrawdowns(equityCurve).averageDrawdown;
const avgReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
return averageDrawdown === 0 ? 0 : (avgReturn - riskFreeRate) / averageDrawdown;
}
/**
* Calculate Ulcer Index
*/
export function ulcerIndex(equityCurve: Array<{ value: number; date: Date }>): number {
let sumSquaredDrawdown = 0;
let peak = equityCurve[0].value;
for (const point of equityCurve) {
peak = Math.max(peak, point.value);
const drawdownPercent = (peak - point.value) / peak * 100;
sumSquaredDrawdown += drawdownPercent * drawdownPercent;
}
return Math.sqrt(sumSquaredDrawdown / equityCurve.length);
}
/**
* Calculate Ulcer Performance Index (UPI)
*/
export function ulcerPerformanceIndex(returns: number[], equityCurve: Array<{ value: number; date: Date }>, riskFreeRate: number = 0): number {
const ui = ulcerIndex(equityCurve);
const avgReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
return ui === 0 ? 0 : (avgReturn - riskFreeRate) / ui;
}
/**
* Calculate Information Ratio
*/
export function informationRatio(portfolioReturns: number[], benchmarkReturns: number[]): number {
if (portfolioReturns.length !== benchmarkReturns.length) {
throw new Error("Portfolio and benchmark returns must have the same length.");
}
const excessReturns = portfolioReturns.map((portfolioReturn, index) => portfolioReturn - benchmarkReturns[index]);
const trackingError = calculateVolatility(excessReturns);
const avgExcessReturn = excessReturns.reduce((sum, ret) => sum + ret, 0) / excessReturns.length;
return trackingError === 0 ? 0 : avgExcessReturn / trackingError;
}
/**
* Calculate Treynor Ratio
*/
export function treynorRatio(portfolioReturns: number[], marketReturns: number[], riskFreeRate: number): number {
const beta = calculateBeta(portfolioReturns, marketReturns);
const avgPortfolioReturn = portfolioReturns.reduce((sum, ret) => sum + ret, 0) / portfolioReturns.length;
return beta === 0 ? 0 : (avgPortfolioReturn - riskFreeRate) / beta;
}
/**
* Calculate Jensen's Alpha (same as Alpha, but included for clarity)
*/
export function jensensAlpha(portfolioReturns: number[], marketReturns: number[], riskFreeRate: number): number {
return calculateAlpha(portfolioReturns, marketReturns, riskFreeRate);
}
/**
* Calculate Capture Ratio (Up Capture and Down Capture)
*/
export function captureRatio(portfolioReturns: number[], benchmarkReturns: number[]): { upCaptureRatio: number; downCaptureRatio: number } {
let upCapture = 0;
let downCapture = 0;
let upMarketPeriods = 0;
let downMarketPeriods = 0;
for (let i = 0; i < portfolioReturns.length; i++) {
if (benchmarkReturns[i] > 0) {
upCapture += portfolioReturns[i];
upMarketPeriods++;
} else if (benchmarkReturns[i] < 0) {
downCapture += portfolioReturns[i];
downMarketPeriods++;
}
}
const upCaptureRatio = upMarketPeriods > 0 ? (upCapture / upMarketPeriods) / (benchmarkReturns.filter(r => r > 0).reduce((sum, r) => sum + r, 0) / upMarketPeriods) : 0;
const downCaptureRatio = downMarketPeriods > 0 ? (downCapture / downMarketPeriods) / (benchmarkReturns.filter(r => r < 0).reduce((sum, r) => sum + r, 0) / downMarketPeriods) : 0;
return { upCaptureRatio, downCaptureRatio };
}
/**
* Calculate Sortino Ratio
*/
export function sortinoRatio(returns: number[], riskFreeRate: number = 0): number {
const avgReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
const downsideReturns = returns.filter(ret => ret < riskFreeRate);
const downsideDeviation = Math.sqrt(
downsideReturns.reduce((sum, ret) => sum + Math.pow(ret - riskFreeRate, 2), 0) / returns.length
);
return downsideDeviation === 0 ? 0 : (avgReturn - riskFreeRate) / downsideDeviation;
}
/**
* Calculate Tail Ratio
*/
export function tailRatio(returns: number[], tailPercent: number = 0.1): number {
const numReturns = returns.length;
const tailSize = Math.floor(numReturns * tailPercent);
if (tailSize === 0) return 0;
const sortedReturns = [...returns].sort((a, b) => a - b);
const worstTail = sortedReturns.slice(0, tailSize);
const bestTail = sortedReturns.slice(numReturns - tailSize);
const avgWorst = worstTail.reduce((sum, ret) => sum + ret, 0) / tailSize;
const avgBest = bestTail.reduce((sum, ret) => sum + ret, 0) / tailSize;
return avgWorst === 0 ? 0 : avgBest / Math.abs(avgWorst);
}
/**
* Calculate Value at Risk (VaR)
*/
export function valueAtRisk(returns: number[], confidenceLevel: number = 0.05): number {
const sortedReturns = [...returns].sort((a, b) => a - b);
const varIndex = Math.floor(confidenceLevel * returns.length);
return sortedReturns[varIndex];
}
/**
* Calculate Conditional Value at Risk (CVaR) / Expected Shortfall
*/
export function conditionalValueAtRisk(returns: number[], confidenceLevel: number = 0.05): number {
const sortedReturns = [...returns].sort((a, b) => a - b);
const varIndex = Math.floor(confidenceLevel * returns.length);
const tailReturns = sortedReturns.slice(0, varIndex + 1);
return tailReturns.reduce((sum, ret) => sum + ret, 0) / tailReturns.length;
}
/**
* Calculate Rolling Beta
*/
export function calculateRollingBeta(portfolioReturns: number[], marketReturns: number[], windowSize: number): number[] {
if (portfolioReturns.length !== marketReturns.length || portfolioReturns.length < windowSize) return [];
const rollingBetas: number[] = [];
for (let i = windowSize; i <= portfolioReturns.length; i++) {
const portfolioWindow = portfolioReturns.slice(i - windowSize, i);
const marketWindow = marketReturns.slice(i - windowSize, i);
rollingBetas.push(calculateBeta(portfolioWindow, marketWindow));
}
return rollingBetas;
}
/**
* Calculate Rolling Alpha
*/
export function calculateRollingAlpha(portfolioReturns: number[], marketReturns: number[], riskFreeRate: number, windowSize: number): number[] {
if (portfolioReturns.length !== marketReturns.length || portfolioReturns.length < windowSize) return [];
const rollingAlphas: number[] = [];
for (let i = windowSize; i <= portfolioReturns.length; i++) {
const portfolioWindow = portfolioReturns.slice(i - windowSize, i);
const marketWindow = marketReturns.slice(i - windowSize, i);
rollingAlphas.push(calculateAlpha(portfolioWindow, marketWindow, riskFreeRate));
}
return rollingAlphas;
}
/**
* Calculate Time Weighted Rate of Return (TWRR)
*/
export function timeWeightedRateOfReturn(cashFlows: Array<{ amount: number; date: Date; value: number }>): number {
let totalReturn = 1;
let previousValue = cashFlows[0].value;
for (let i = 1; i < cashFlows.length; i++) {
const current = cashFlows[i];
const periodReturn = (current.value - previousValue - current.amount) / (previousValue + current.amount);
totalReturn *= (1 + periodReturn);
previousValue = current.value;
}
return totalReturn - 1;
}
/**
* Calculate Money Weighted Rate of Return (MWRR) - Approximation using IRR
*/
export function moneyWeightedRateOfReturn(cashFlows: Array<{ amount: number; date: Date; value: number }>): number {
// Approximate MWRR using Internal Rate of Return (IRR)
// This requires a numerical method or library for accurate IRR calculation
// This is a simplified example and may not be accurate for all cases
let totalCashFlow = 0;
let totalWeightedCashFlow = 0;
const startDate = cashFlows[0].date.getTime();
for (const cf of cashFlows) {
const timeDiff = (cf.date.getTime() - startDate) / (1000 * 60 * 60 * 24 * 365); // Years
totalCashFlow += cf.amount;
totalWeightedCashFlow += cf.amount * timeDiff;
}
// Simplified approximation: MWRR ≈ totalCashFlow / totalWeightedCashFlow - 1
return totalCashFlow / totalWeightedCashFlow - 1;
}
// Helper functions
function calculateSharpeRatio(returns: number[], riskFreeRate: number = 0): number {
if (returns.length < 2) return 0;
const avgReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
const variance = returns.reduce((sum, ret) => sum + Math.pow(ret - avgReturn, 2), 0) / (returns.length - 1);
const stdDev = Math.sqrt(variance);
return stdDev === 0 ? 0 : (avgReturn - riskFreeRate) / stdDev;
}
function calculateVolatility(returns: number[]): number {
if (returns.length < 2) return 0;
const mean = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
const variance = returns.reduce((sum, ret) => sum + Math.pow(ret - mean, 2), 0) / (returns.length - 1);
return Math.sqrt(variance);
}
function calculateBeta(portfolioReturns: number[], marketReturns: number[]): number {
if (portfolioReturns.length !== marketReturns.length || portfolioReturns.length < 2) return 0;
const portfolioMean = portfolioReturns.reduce((sum, ret) => sum + ret, 0) / portfolioReturns.length;
const marketMean = marketReturns.reduce((sum, ret) => sum + ret, 0) / marketReturns.length;
let covariance = 0;
let marketVariance = 0;
for (let i = 0; i < portfolioReturns.length; i++) {
const portfolioDiff = portfolioReturns[i] - portfolioMean;
const marketDiff = marketReturns[i] - marketMean;
covariance += portfolioDiff * marketDiff;
marketVariance += marketDiff * marketDiff;
}
covariance /= (portfolioReturns.length - 1);
marketVariance /= (marketReturns.length - 1);
return marketVariance === 0 ? 0 : covariance / marketVariance;
}
function calculateAlpha(
portfolioReturns: number[],
marketReturns: number[],
riskFreeRate: number
): number {
const portfolioMean = portfolioReturns.reduce((sum, ret) => sum + ret, 0) / portfolioReturns.length;
const marketMean = marketReturns.reduce((sum, ret) => sum + ret, 0) / marketReturns.length;
const beta = calculateBeta(portfolioReturns, marketReturns);
return portfolioMean - (riskFreeRate + beta * (marketMean - riskFreeRate));
}
function calculateSkewness(returns: number[]): number {
if (returns.length < 3) return 0;
const mean = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
const variance = returns.reduce((sum, ret) => sum + Math.pow(ret - mean, 2), 0) / returns.length;
const stdDev = Math.sqrt(variance);
if (stdDev === 0) return 0;
const skew = returns.reduce((sum, ret) => sum + Math.pow((ret - mean) / stdDev, 3), 0) / returns.length;
return skew;
}
function calculateKurtosis(returns: number[]): number {
if (returns.length < 4) return 0;
const mean = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
const variance = returns.reduce((sum, ret) => sum + Math.pow(ret - mean, 2), 0) / returns.length;
const stdDev = Math.sqrt(variance);
if (stdDev === 0) return 0;
const kurt = returns.reduce((sum, ret) => sum + Math.pow((ret - mean) / stdDev, 4), 0) / returns.length;
return kurt - 3; // Excess kurtosis
}
function aggregateMonthlyReturns(returns: Array<{ return: number; date: Date }>): number[] {
const monthlyReturns: { [key: string]: number } = {};
for (const ret of returns) {
const monthKey = `${ret.date.getFullYear()}-${ret.date.getMonth()}`;
if (!monthlyReturns[monthKey]) {
monthlyReturns[monthKey] = 1;
}
monthlyReturns[monthKey] *= (1 + ret.return);
}
return Object.values(monthlyReturns).map(cumReturn => cumReturn - 1);
}