UNPKG

quantitivecalc

Version:

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

282 lines (281 loc) 12.5 kB
"use strict"; /** * Performance Metrics Utilities * * Functions: * - calculateCompoundReturns: Calculates compound values from a series of daily returns. * - calculateDailyReturns: Calculates daily returns from a series of values. * - calculateAnnualizedReturns: Converts returns to annualized format * - calculateAlpha: Calculates alpha (excess return vs benchmark) * - calculateInformationRatio: Active return per unit of tracking error * - calculateCalmarRatio: Annual return divided by maximum drawdown * - calculateSortinoRatio: Return adjusted for downside deviation * * All functions operate on arrays of objects (list of dicts) and allow you to specify source/result columns. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.calculateCompoundReturns = calculateCompoundReturns; exports.calculateDailyReturns = calculateDailyReturns; exports.calculateAnnualizedReturns = calculateAnnualizedReturns; exports.calculateAlpha = calculateAlpha; exports.calculateInformationRatio = calculateInformationRatio; exports.calculateCalmarRatio = calculateCalmarRatio; exports.calculateSortinoRatio = calculateSortinoRatio; function calculateCompoundReturns(data, returnsColumn = 'dailyReturn', resultColumn, initialValue = 1) { if (!data || data.length === 0) { return []; } // Create a copy of the data to avoid mutating the original const result = data.map(row => ({ ...row })); let compoundValue = initialValue; for (let i = 0; i < result.length; i++) { const dailyReturn = result[i][returnsColumn]; if (typeof dailyReturn === 'number' && !isNaN(dailyReturn)) { // Compound formula: previous_value * (1 + daily_return) compoundValue = compoundValue * (1 + dailyReturn); } // If daily return is null/invalid, keep the same compound value result[i][resultColumn] = compoundValue; } return result; } function calculateDailyReturns(data, sourceColumn, resultColumn = 'dailyReturn', initialValue = 0) { if (!data || data.length === 0) { return []; } // Create a copy of the data to avoid mutating the original const result = data.map(row => ({ ...row })); // Calculate daily returns for each row starting from index 1 for (let i = 0; i < result.length; i++) { if (i === 0) { // First row has no previous value, so daily return is null/undefined result[i][resultColumn] = initialValue; } else { const currentValue = result[i][sourceColumn]; const previousValue = result[i - 1][sourceColumn]; // Check if both values are valid numbers if (typeof currentValue === 'number' && typeof previousValue === 'number' && previousValue !== 0 && !isNaN(currentValue) && !isNaN(previousValue)) { // Daily return formula: (current - previous) / previous const dailyReturn = (currentValue - previousValue) / previousValue; result[i][resultColumn] = dailyReturn; } else { // Invalid data - set to null result[i][resultColumn] = null; } } } return result; } function calculateAnnualizedReturns(data, returnsColumn, resultColumn, frequency = 'daily', method = 'compound') { if (!data || data.length === 0) { return []; } const result = data.map(row => ({ ...row })); // Periods per year based on frequency const periodsPerYear = { daily: 252, // Trading days weekly: 52, monthly: 12 }; const periods = periodsPerYear[frequency]; for (let i = 0; i < result.length; i++) { const returnValue = result[i][returnsColumn]; if (typeof returnValue === 'number' && !isNaN(returnValue)) { let annualizedReturn; if (method === 'compound') { // Compound: (1 + return)^periods - 1 annualizedReturn = Math.pow(1 + returnValue, periods) - 1; } else { // Simple: return * periods annualizedReturn = returnValue * periods; } result[i][resultColumn] = annualizedReturn; } else { result[i][resultColumn] = null; } } return result; } function calculateAlpha(data, assetReturnsColumn, benchmarkReturnsColumn, resultColumn, windowSize = 252, // 1 year for daily data riskFreeRate = 0.02 // 2% annual ) { if (!data || data.length === 0) { return []; } const result = data.map(row => ({ ...row })); const dailyRiskFreeRate = riskFreeRate / 252; for (let i = 0; i < result.length; i++) { if (i < windowSize - 1) { result[i][resultColumn] = null; } else { const assetReturns = []; const benchmarkReturns = []; // Collect returns for the window for (let j = i - windowSize + 1; j <= i; j++) { const assetReturn = result[j][assetReturnsColumn]; const benchmarkReturn = result[j][benchmarkReturnsColumn]; if (typeof assetReturn === 'number' && !isNaN(assetReturn) && typeof benchmarkReturn === 'number' && !isNaN(benchmarkReturn)) { assetReturns.push(assetReturn - dailyRiskFreeRate); benchmarkReturns.push(benchmarkReturn - dailyRiskFreeRate); } } if (assetReturns.length > 10) { // Need sufficient data points // Calculate beta (slope of regression) const n = assetReturns.length; const sumX = benchmarkReturns.reduce((a, b) => a + b, 0); const sumY = assetReturns.reduce((a, b) => a + b, 0); const sumXY = benchmarkReturns.reduce((sum, x, idx) => sum + x * assetReturns[idx], 0); const sumXX = benchmarkReturns.reduce((sum, x) => sum + x * x, 0); const beta = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX); const avgAssetReturn = sumY / n; const avgBenchmarkReturn = sumX / n; // Alpha = Asset Return - Beta * Benchmark Return (annualized) const alpha = (avgAssetReturn - beta * avgBenchmarkReturn) * 252; result[i][resultColumn] = alpha; } else { result[i][resultColumn] = null; } } } return result; } function calculateInformationRatio(data, assetReturnsColumn, benchmarkReturnsColumn, resultColumn, windowSize = 252) { if (!data || data.length === 0) { return []; } const result = data.map(row => ({ ...row })); for (let i = 0; i < result.length; i++) { if (i < windowSize - 1) { result[i][resultColumn] = null; } else { const activeReturns = []; // Calculate active returns (asset - benchmark) for (let j = i - windowSize + 1; j <= i; j++) { const assetReturn = result[j][assetReturnsColumn]; const benchmarkReturn = result[j][benchmarkReturnsColumn]; if (typeof assetReturn === 'number' && !isNaN(assetReturn) && typeof benchmarkReturn === 'number' && !isNaN(benchmarkReturn)) { activeReturns.push(assetReturn - benchmarkReturn); } } if (activeReturns.length > 1) { // Calculate mean and standard deviation of active returns const meanActiveReturn = activeReturns.reduce((sum, val) => sum + val, 0) / activeReturns.length; const variance = activeReturns.reduce((sum, val) => sum + Math.pow(val - meanActiveReturn, 2), 0) / (activeReturns.length - 1); const trackingError = Math.sqrt(variance); // Information Ratio = Mean Active Return / Tracking Error (annualized) const informationRatio = trackingError > 0 ? (meanActiveReturn / trackingError) * Math.sqrt(252) : 0; result[i][resultColumn] = informationRatio; } else { result[i][resultColumn] = null; } } } return result; } function calculateCalmarRatio(data, returnsColumn, priceColumn, resultColumn, windowSize = 252) { if (!data || data.length === 0) { return []; } const result = data.map(row => ({ ...row })); for (let i = 0; i < result.length; i++) { if (i < windowSize - 1) { result[i][resultColumn] = null; } else { // Calculate annualized return for the window const windowReturns = []; for (let j = i - windowSize + 1; j <= i; j++) { const returnValue = result[j][returnsColumn]; if (typeof returnValue === 'number' && !isNaN(returnValue)) { windowReturns.push(returnValue); } } // Calculate maximum drawdown for the window let peak = -Infinity; let maxDrawdown = 0; for (let j = i - windowSize + 1; j <= i; j++) { const price = result[j][priceColumn]; if (typeof price === 'number' && !isNaN(price)) { if (price > peak) peak = price; const drawdown = (peak - price) / peak; if (drawdown > maxDrawdown) maxDrawdown = drawdown; } } if (windowReturns.length > 0 && maxDrawdown > 0) { // Annualized return const avgReturn = windowReturns.reduce((sum, val) => sum + val, 0) / windowReturns.length; const annualizedReturn = avgReturn * 252; // Calmar Ratio = Annualized Return / Maximum Drawdown const calmarRatio = annualizedReturn / maxDrawdown; result[i][resultColumn] = calmarRatio; } else { result[i][resultColumn] = null; } } } return result; } function calculateSortinoRatio(data, returnsColumn, resultColumn, windowSize = 252, riskFreeRate = 0.02, targetReturn = null // If null, uses risk-free rate ) { if (!data || data.length === 0) { return []; } const result = data.map(row => ({ ...row })); const dailyRiskFreeRate = riskFreeRate / 252; const dailyTargetReturn = targetReturn ? targetReturn / 252 : dailyRiskFreeRate; for (let i = 0; i < result.length; i++) { if (i < windowSize - 1) { result[i][resultColumn] = null; } else { const windowReturns = []; const downsideReturns = []; // Collect returns and downside returns for (let j = i - windowSize + 1; j <= i; j++) { const returnValue = result[j][returnsColumn]; if (typeof returnValue === 'number' && !isNaN(returnValue)) { windowReturns.push(returnValue); // Only include returns below target for downside deviation if (returnValue < dailyTargetReturn) { downsideReturns.push(returnValue - dailyTargetReturn); } } } if (windowReturns.length > 1) { // Calculate mean return const meanReturn = windowReturns.reduce((sum, val) => sum + val, 0) / windowReturns.length; // Calculate downside deviation let downsideDeviation = 0; if (downsideReturns.length > 0) { const downsideVariance = downsideReturns.reduce((sum, val) => sum + val * val, 0) / windowReturns.length; downsideDeviation = Math.sqrt(downsideVariance); } // Sortino Ratio = (Mean Return - Target Return) / Downside Deviation (annualized) const excessReturn = meanReturn - dailyTargetReturn; const sortinoRatio = downsideDeviation > 0 ? (excessReturn / downsideDeviation) * Math.sqrt(252) : 0; result[i][resultColumn] = sortinoRatio; } else { result[i][resultColumn] = null; } } } return result; }