meridianalgo-js
Version:
Advanced algorithmic trading library for Node.js & TypeScript with 100+ technical indicators, pattern recognition, and risk management tools.
482 lines • 17.9 kB
JavaScript
/**
* Performance Metrics and Risk Management
*
* This module provides performance metrics and risk management utilities for
* quantitative finance applications including Sharpe ratio, Sortino ratio,
* maximum drawdown, and other risk-adjusted return metrics.
*
* @fileoverview Performance metrics and risk management utilities
* @author MeridianAlgo
* @version 1.0.0
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.PerformanceMetrics = exports.performanceAnalysis = exports.profitFactor = exports.averageWinLoss = exports.winRate = exports.trackingError = exports.jensensAlpha = exports.treynorRatio = exports.alpha = exports.beta = exports.conditionalValueAtRisk = exports.valueAtRisk = exports.informationRatio = exports.calmarRatio = exports.maxDrawdown = exports.sortinoRatio = exports.sharpeRatio = void 0;
/**
* Calculate returns from price data
*
* @param prices - Array of price data
* @param method - Method for calculating returns ('simple' or 'log')
* @returns Array of returns
*/
function calculateReturns(prices, method = 'log') {
if (prices.length < 2)
return [];
const returns = [];
for (let i = 1; i < prices.length; i++) {
if (method === 'log') {
returns.push(Math.log(prices[i] / prices[i - 1]));
}
else {
returns.push((prices[i] - prices[i - 1]) / prices[i - 1]);
}
}
return returns;
}
/**
* Calculate cumulative returns
*
* @param returns - Array of returns
* @returns Array of cumulative returns
*/
function calculateCumulativeReturns(returns) {
if (returns.length === 0)
return [];
const cumulative = [1 + returns[0]];
for (let i = 1; i < returns.length; i++) {
cumulative.push(cumulative[i - 1] * (1 + returns[i]));
}
return cumulative;
}
/**
* Sharpe Ratio
*
* The Sharpe ratio measures the risk-adjusted return of an investment.
* It's calculated as (portfolio return - risk-free rate) / standard deviation of returns.
*
* @param returns - Array of returns
* @param riskFreeRate - Risk-free rate (default: 0.02 for 2% annual)
* @param annualized - Whether to annualize the ratio (default: true)
* @returns Sharpe ratio value
*
* @example
* ```typescript
* const returns = [0.01, 0.02, -0.01, 0.03, 0.015];
* const sharpe = PerformanceMetrics.sharpeRatio(returns);
* ```
*/
function sharpeRatio(returns, riskFreeRate = 0.02, annualized = true) {
if (returns.length === 0)
return 0;
const meanReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
const variance = returns.reduce((sum, ret) => sum + Math.pow(ret - meanReturn, 2), 0) / (returns.length - 1);
const stdDev = Math.sqrt(variance);
if (stdDev === 0)
return 0;
let excessReturn = meanReturn - (riskFreeRate / 252); // Daily risk-free rate
if (annualized) {
excessReturn *= 252; // Annualize
return excessReturn / (stdDev * Math.sqrt(252));
}
return excessReturn / stdDev;
}
exports.sharpeRatio = sharpeRatio;
/**
* Sortino Ratio
*
* The Sortino ratio is similar to the Sharpe ratio but only considers downside deviation.
* It's calculated as (portfolio return - risk-free rate) / downside deviation.
*
* @param returns - Array of returns
* @param riskFreeRate - Risk-free rate (default: 0.02 for 2% annual)
* @param annualized - Whether to annualize the ratio (default: true)
* @returns Sortino ratio value
*/
function sortinoRatio(returns, riskFreeRate = 0.02, annualized = true) {
if (returns.length === 0)
return 0;
const meanReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
const downsideReturns = returns.filter(ret => ret < 0);
if (downsideReturns.length === 0)
return Infinity;
const downsideVariance = downsideReturns.reduce((sum, ret) => sum + Math.pow(ret, 2), 0) / downsideReturns.length;
const downsideDeviation = Math.sqrt(downsideVariance);
if (downsideDeviation === 0)
return 0;
let excessReturn = meanReturn - (riskFreeRate / 252); // Daily risk-free rate
if (annualized) {
excessReturn *= 252; // Annualize
return excessReturn / (downsideDeviation * Math.sqrt(252));
}
return excessReturn / downsideDeviation;
}
exports.sortinoRatio = sortinoRatio;
/**
* Maximum Drawdown
*
* Maximum drawdown is the largest peak-to-trough decline in the value of a portfolio.
* It's expressed as a percentage and measures the worst loss from a peak.
*
* @param prices - Array of price data
* @returns Object containing maximum drawdown and related metrics
*/
function maxDrawdown(prices) {
if (prices.length === 0) {
return {
maxDrawdown: 0,
maxDrawdownPercent: 0,
drawdownStart: 0,
drawdownEnd: 0,
recoveryTime: 0
};
}
let maxDrawdown = 0;
let maxDrawdownPercent = 0;
let drawdownStart = 0;
let drawdownEnd = 0;
let peak = prices[0];
let peakIndex = 0;
for (let i = 1; i < prices.length; i++) {
if (prices[i] > peak) {
peak = prices[i];
peakIndex = i;
}
else {
const drawdown = peak - prices[i];
const drawdownPercent = (drawdown / peak) * 100;
if (drawdown > maxDrawdown) {
maxDrawdown = drawdown;
maxDrawdownPercent = drawdownPercent;
drawdownStart = peakIndex;
drawdownEnd = i;
}
}
}
const recoveryTime = drawdownEnd - drawdownStart;
return {
maxDrawdown,
maxDrawdownPercent,
drawdownStart,
drawdownEnd,
recoveryTime
};
}
exports.maxDrawdown = maxDrawdown;
/**
* Calmar Ratio
*
* The Calmar ratio is the annualized return divided by the maximum drawdown.
* It measures risk-adjusted returns relative to the worst loss.
*
* @param returns - Array of returns
* @param maxDrawdownPercent - Maximum drawdown percentage
* @returns Calmar ratio value
*/
function calmarRatio(returns, maxDrawdownPercent) {
if (returns.length === 0 || maxDrawdownPercent === 0)
return 0;
const annualizedReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length * 252;
return annualizedReturn / (maxDrawdownPercent / 100);
}
exports.calmarRatio = calmarRatio;
/**
* Information Ratio
*
* The Information ratio measures the excess return per unit of tracking error.
* It's calculated as (portfolio return - benchmark return) / tracking error.
*
* @param portfolioReturns - Array of portfolio returns
* @param benchmarkReturns - Array of benchmark returns
* @returns Information ratio value
*/
function informationRatio(portfolioReturns, benchmarkReturns) {
if (portfolioReturns.length !== benchmarkReturns.length || portfolioReturns.length === 0) {
return 0;
}
const excessReturns = portfolioReturns.map((ret, i) => ret - benchmarkReturns[i]);
const meanExcessReturn = excessReturns.reduce((sum, ret) => sum + ret, 0) / excessReturns.length;
const trackingError = Math.sqrt(excessReturns.reduce((sum, ret) => sum + Math.pow(ret - meanExcessReturn, 2), 0) / (excessReturns.length - 1));
if (trackingError === 0)
return 0;
return meanExcessReturn / trackingError;
}
exports.informationRatio = informationRatio;
/**
* Value at Risk (VaR)
*
* VaR measures the potential loss in value of a portfolio over a defined period
* for a given confidence interval.
*
* @param returns - Array of returns
* @param confidence - Confidence level (default: 0.05 for 95% VaR)
* @param method - Method for calculating VaR ('historical' or 'parametric')
* @returns VaR value
*/
function valueAtRisk(returns, confidence = 0.05, method = 'historical') {
if (returns.length === 0)
return 0;
if (method === 'historical') {
const sortedReturns = [...returns].sort((a, b) => a - b);
const index = Math.floor(confidence * sortedReturns.length);
return Math.abs(sortedReturns[index]);
}
else {
// Parametric method (assuming normal distribution)
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);
const stdDev = Math.sqrt(variance);
// Z-score for the confidence level (approximate)
const zScore = confidence === 0.05 ? 1.645 : confidence === 0.01 ? 2.326 : 1.96;
return Math.abs(mean - zScore * stdDev);
}
}
exports.valueAtRisk = valueAtRisk;
/**
* Conditional Value at Risk (CVaR)
*
* CVaR, also known as Expected Shortfall, is the expected loss given that
* the loss exceeds the VaR threshold.
*
* @param returns - Array of returns
* @param confidence - Confidence level (default: 0.05 for 95% CVaR)
* @returns CVaR value
*/
function conditionalValueAtRisk(returns, confidence = 0.05) {
if (returns.length === 0)
return 0;
const varValue = valueAtRisk(returns, confidence, 'historical');
const tailReturns = returns.filter(ret => ret <= -varValue);
if (tailReturns.length === 0)
return varValue;
return Math.abs(tailReturns.reduce((sum, ret) => sum + ret, 0) / tailReturns.length);
}
exports.conditionalValueAtRisk = conditionalValueAtRisk;
/**
* Beta
*
* Beta measures the sensitivity of a portfolio's returns to market returns.
* A beta of 1 means the portfolio moves with the market.
*
* @param portfolioReturns - Array of portfolio returns
* @param marketReturns - Array of market returns
* @returns Beta value
*/
function beta(portfolioReturns, marketReturns) {
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;
}
if (marketVariance === 0)
return 0;
return covariance / marketVariance;
}
exports.beta = beta;
/**
* Alpha
*
* Alpha measures the excess return of a portfolio relative to the return
* predicted by the Capital Asset Pricing Model (CAPM).
*
* @param portfolioReturns - Array of portfolio returns
* @param marketReturns - Array of market returns
* @param riskFreeRate - Risk-free rate (default: 0.02 for 2% annual)
* @returns Alpha value
*/
function alpha(portfolioReturns, marketReturns, riskFreeRate = 0.02) {
if (portfolioReturns.length !== marketReturns.length || portfolioReturns.length === 0) {
return 0;
}
const portfolioMean = portfolioReturns.reduce((sum, ret) => sum + ret, 0) / portfolioReturns.length;
const marketMean = marketReturns.reduce((sum, ret) => sum + ret, 0) / marketReturns.length;
const betaValue = beta(portfolioReturns, marketReturns);
const dailyRiskFreeRate = riskFreeRate / 252;
return portfolioMean - (dailyRiskFreeRate + betaValue * (marketMean - dailyRiskFreeRate));
}
exports.alpha = alpha;
/**
* Treynor Ratio
*
* The Treynor ratio measures risk-adjusted returns relative to systematic risk (beta).
* It's calculated as (portfolio return - risk-free rate) / beta.
*
* @param returns - Array of returns
* @param betaValue - Beta value of the portfolio
* @param riskFreeRate - Risk-free rate (default: 0.02 for 2% annual)
* @returns Treynor ratio value
*/
function treynorRatio(returns, betaValue, riskFreeRate = 0.02) {
if (returns.length === 0 || betaValue === 0)
return 0;
const meanReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
const annualizedReturn = meanReturn * 252;
const annualizedRiskFreeRate = riskFreeRate;
return (annualizedReturn - annualizedRiskFreeRate) / betaValue;
}
exports.treynorRatio = treynorRatio;
/**
* Jensen's Alpha
*
* Jensen's Alpha is the same as the regular alpha but is often used in the context
* of evaluating fund managers' performance.
*
* @param portfolioReturns - Array of portfolio returns
* @param marketReturns - Array of market returns
* @param riskFreeRate - Risk-free rate (default: 0.02 for 2% annual)
* @returns Jensen's Alpha value
*/
function jensensAlpha(portfolioReturns, marketReturns, riskFreeRate = 0.02) {
return alpha(portfolioReturns, marketReturns, riskFreeRate);
}
exports.jensensAlpha = jensensAlpha;
/**
* Tracking Error
*
* Tracking error measures the standard deviation of the difference between
* portfolio returns and benchmark returns.
*
* @param portfolioReturns - Array of portfolio returns
* @param benchmarkReturns - Array of benchmark returns
* @returns Tracking error value
*/
function trackingError(portfolioReturns, benchmarkReturns) {
if (portfolioReturns.length !== benchmarkReturns.length || portfolioReturns.length < 2) {
return 0;
}
const excessReturns = portfolioReturns.map((ret, i) => ret - benchmarkReturns[i]);
const meanExcessReturn = excessReturns.reduce((sum, ret) => sum + ret, 0) / excessReturns.length;
const variance = excessReturns.reduce((sum, ret) => sum + Math.pow(ret - meanExcessReturn, 2), 0) / (excessReturns.length - 1);
return Math.sqrt(variance);
}
exports.trackingError = trackingError;
/**
* Win Rate
*
* Calculates the percentage of positive returns in a series.
*
* @param returns - Array of returns
* @returns Win rate as a percentage
*/
function winRate(returns) {
if (returns.length === 0)
return 0;
const positiveReturns = returns.filter(ret => ret > 0).length;
return (positiveReturns / returns.length) * 100;
}
exports.winRate = winRate;
/**
* Average Win/Loss
*
* Calculates the average win and average loss from a series of returns.
*
* @param returns - Array of returns
* @returns Object containing average win and average loss
*/
function averageWinLoss(returns) {
if (returns.length === 0) {
return { averageWin: 0, averageLoss: 0 };
}
const wins = returns.filter(ret => ret > 0);
const losses = returns.filter(ret => ret < 0);
const averageWin = wins.length > 0 ? wins.reduce((sum, ret) => sum + ret, 0) / wins.length : 0;
const averageLoss = losses.length > 0 ? Math.abs(losses.reduce((sum, ret) => sum + ret, 0) / losses.length) : 0;
return { averageWin, averageLoss };
}
exports.averageWinLoss = averageWinLoss;
/**
* Profit Factor
*
* Profit factor is the ratio of gross profit to gross loss.
* A value greater than 1 indicates profitability.
*
* @param returns - Array of returns
* @returns Profit factor value
*/
function profitFactor(returns) {
if (returns.length === 0)
return 0;
const wins = returns.filter(ret => ret > 0);
const losses = returns.filter(ret => ret < 0);
const grossProfit = wins.reduce((sum, ret) => sum + ret, 0);
const grossLoss = Math.abs(losses.reduce((sum, ret) => sum + ret, 0));
if (grossLoss === 0)
return grossProfit > 0 ? Infinity : 0;
return grossProfit / grossLoss;
}
exports.profitFactor = profitFactor;
/**
* Comprehensive performance analysis
*
* @param prices - Array of price data
* @param benchmarkPrices - Array of benchmark price data (optional)
* @param riskFreeRate - Risk-free rate (default: 0.02 for 2% annual)
* @returns Object containing all performance metrics
*/
function performanceAnalysis(prices, benchmarkPrices, riskFreeRate = 0.02) {
const returns = calculateReturns(prices);
const totalReturn = prices.length > 0 ? (prices[prices.length - 1] / prices[0]) - 1 : 0;
const annualizedReturn = returns.length > 0 ? Math.pow(1 + totalReturn, 252 / returns.length) - 1 : 0;
const volatility = returns.length > 0 ? Math.sqrt(returns.reduce((sum, ret) => sum + Math.pow(ret, 0), 0) / returns.length) * Math.sqrt(252) : 0;
const sharpe = sharpeRatio(returns, riskFreeRate);
const sortino = sortinoRatio(returns, riskFreeRate);
const maxDD = maxDrawdown(prices);
const calmar = calmarRatio(returns, maxDD.maxDrawdownPercent);
const win = winRate(returns);
const profit = profitFactor(returns);
const avgWinLoss = averageWinLoss(returns);
const var95 = valueAtRisk(returns, 0.05);
const cvar95 = conditionalValueAtRisk(returns, 0.05);
const result = {
totalReturn,
annualizedReturn,
volatility,
sharpeRatio: sharpe,
sortinoRatio: sortino,
maxDrawdown: maxDD.maxDrawdownPercent,
calmarRatio: calmar,
winRate: win,
profitFactor: profit,
averageWinLoss: avgWinLoss,
var95,
cvar95
};
if (benchmarkPrices && benchmarkPrices.length > 0) {
const benchmarkReturns = calculateReturns(benchmarkPrices);
result.beta = beta(returns, benchmarkReturns);
result.alpha = alpha(returns, benchmarkReturns, riskFreeRate);
result.informationRatio = informationRatio(returns, benchmarkReturns);
result.trackingError = trackingError(returns, benchmarkReturns);
}
return result;
}
exports.performanceAnalysis = performanceAnalysis;
/**
* Collection of performance metrics and risk management utilities
*/
exports.PerformanceMetrics = {
sharpeRatio,
sortinoRatio,
maxDrawdown,
calmarRatio,
informationRatio,
valueAtRisk,
conditionalValueAtRisk,
beta,
alpha,
treynorRatio,
jensensAlpha,
trackingError,
winRate,
averageWinLoss,
profitFactor,
performanceAnalysis
};
//# sourceMappingURL=performance.js.map
;