import numpy as np
def _normCdf(x):
"""Standard normal CDF approximation."""
return 0.5 * (1.0 + np.tanh(x / np.sqrt(2.0) * 0.7978845608))
def _normPpf(p):
"""Standard normal percent point function (inverse CDF)."""
if p <= 0 or p >= 1:
return np.nan
if p < 0.5:
sign = -1
p = 1 - p
else:
sign = 1
t = np.sqrt(-2 * np.log(1 - p))
c0 = 2.515517
c1 = 0.802853
c2 = 0.010328
d1 = 1.432788
d2 = 0.189269
d3 = 0.001308
x = t - (c0 + c1 * t + c2 * t**2) / (1 + d1 * t + d2 * t**2 + d3 * t**3)
return sign * x
def _normPdf(x):
"""Standard normal PDF."""
return np.exp(-0.5 * x**2) / np.sqrt(2 * np.pi)
[docs]
def parametricVar(returns, confidence=0.95):
"""
Parametric Value at Risk (assumes normal distribution).
Parameters:
returns: array of returns
confidence: confidence level (default 0.95)
Returns:
VaR estimate
"""
mean = np.mean(returns)
std = np.std(returns)
zScore = _normPpf(1 - confidence)
var = -(mean + zScore * std)
return var
[docs]
def historicalVar(returns, confidence=0.95):
"""
Historical simulation Value at Risk.
Parameters:
returns: array of returns
confidence: confidence level
Returns:
VaR estimate
"""
sortedReturns = np.sort(returns)
index = int((1 - confidence) * len(sortedReturns))
var = -sortedReturns[index]
return var
[docs]
def parametricCvar(returns, confidence=0.95):
"""
Parametric Conditional Value at Risk (assumes normal).
Parameters:
returns: array of returns
confidence: confidence level
Returns:
CVaR estimate
"""
mean = np.mean(returns)
std = np.std(returns)
zScore = _normPpf(1 - confidence)
cvar = -(mean + std * (_normPdf(zScore) / (1 - confidence)))
return cvar
[docs]
def historicalCvar(returns, confidence=0.95):
"""
Historical simulation Conditional Value at Risk.
Parameters:
returns: array of returns
confidence: confidence level
Returns:
CVaR estimate
"""
sortedReturns = np.sort(returns)
index = int((1 - confidence) * len(sortedReturns))
tailLosses = sortedReturns[:index + 1]
cvar = -np.mean(tailLosses)
return cvar
[docs]
def expectedShortfall(returns, confidence=0.95):
"""
Expected shortfall (ES), same as historical CVaR.
Parameters:
returns: array of returns
confidence: confidence level
Returns:
ES estimate
"""
return historicalCvar(returns, confidence)
[docs]
def drawdown(returns):
"""
Maximum drawdown.
Parameters:
returns: array of returns
Returns:
maximum drawdown (negative value)
"""
cumReturns = np.cumsum(returns)
peak = np.maximum.accumulate(cumReturns)
drawdowns = (cumReturns - peak) / (peak + 1)
maxDrawdown = np.min(drawdowns)
return maxDrawdown
[docs]
def calmarRatio(returns, riskFreeRate=0):
"""
Calmar ratio: annualized return / max drawdown.
Parameters:
returns: array of returns
riskFreeRate: risk-free rate
Returns:
Calmar ratio
"""
annualizedReturn = np.mean(returns) * 252
maxDrawdown = drawdown(returns)
calmar = annualizedReturn / abs(maxDrawdown) if maxDrawdown != 0 else np.inf
return calmar
[docs]
def sharpeRatio(returns, riskFreeRate=0):
"""
Sharpe ratio.
Parameters:
returns: array of returns
riskFreeRate: risk-free rate per period
Returns:
Sharpe ratio
"""
excessReturns = returns - riskFreeRate
return np.mean(excessReturns) / (np.std(excessReturns) + 1e-10)
[docs]
def sortinoRatio(returns, riskFreeRate=0, target=0):
"""
Sortino ratio (uses downside deviation).
Parameters:
returns: array of returns
riskFreeRate: risk-free rate
target: target return (default 0)
Returns:
Sortino ratio
"""
excessReturns = returns - riskFreeRate
downsideReturns = excessReturns[excessReturns < target]
if len(downsideReturns) == 0:
return np.inf
downsideDev = np.sqrt(np.mean(downsideReturns**2))
return np.mean(excessReturns) / (downsideDev + 1e-10)
[docs]
def omegaRatio(returns, threshold=0.0):
"""
Omega ratio: ratio of gains to losses.
Parameters:
returns: array of returns
threshold: threshold return
Returns:
Omega ratio
"""
gains = returns[returns > threshold] - threshold
losses = threshold - returns[returns < threshold]
sumGains = np.sum(gains) if len(gains) > 0 else 0
sumLosses = np.sum(losses) if len(losses) > 0 else 1e-10
omega = sumGains / sumLosses
return omega
[docs]
def modifiedVar(returns, confidence=0.95):
"""
Modified VaR using Cornish-Fisher expansion for skewness and kurtosis.
Parameters:
returns: array of returns
confidence: confidence level
Returns:
modified VaR
"""
z = _normPpf(confidence)
s = _skew(returns)
k = _kurtosis(returns)
zCf = z + (1 / 6) * (z**2 - 1) * s + (1 / 24) * (z**3 - 3 * z) * (k - 3) - (1 / 36) * (2 * z**3 - 5 * z) * s**2
return -np.mean(returns) + zCf * np.std(returns)
def _skew(x):
"""Sample skewness."""
n = len(x)
mean = np.mean(x)
m2 = np.sum((x - mean)**2) / n
m3 = np.sum((x - mean)**3) / n
return m3 / (m2**1.5 + 1e-10)
def _kurtosis(x):
"""Sample excess kurtosis."""
n = len(x)
mean = np.mean(x)
m2 = np.sum((x - mean)**2) / n
m4 = np.sum((x - mean)**4) / n
return m4 / (m2**2 + 1e-10)
[docs]
def hillTailIndex(returns, k=50):
"""
Hill estimator for tail index (heavy tails).
Parameters:
returns: array of returns
k: number of extreme values
Returns:
tail index (lower = heavier tail)
"""
sortedReturns = -np.sort(-returns)
topK = sortedReturns[:k]
if len(topK) < 2:
return np.nan
return 1 / np.mean(np.log(topK / sortedReturns[k]))
[docs]
def excessKurtosis(returns):
"""
Excess kurtosis.
Parameters:
returns: array of returns
Returns:
excess kurtosis
"""
return _kurtosis(returns)
[docs]
def beta(assetReturns, marketReturns):
"""
Beta coefficient (systematic risk).
Parameters:
assetReturns: asset returns
marketReturns: market returns
Returns:
beta
"""
covariance = np.cov(assetReturns, marketReturns)[0, 1]
marketVariance = np.var(marketReturns)
return covariance / (marketVariance + 1e-10)
[docs]
def treynorRatio(returns, marketReturns, riskFreeRate=0):
"""
Treynor ratio: excess return / beta.
Parameters:
returns: portfolio returns
marketReturns: market returns
riskFreeRate: risk-free rate
Returns:
Treynor ratio
"""
excessReturn = np.mean(returns) - riskFreeRate
portfolioBeta = beta(returns, marketReturns)
return excessReturn / (portfolioBeta + 1e-10)
[docs]
def trackingError(returns, benchmarkReturns):
"""
Tracking error (standard deviation of active returns).
Parameters:
returns: portfolio returns
benchmarkReturns: benchmark returns
Returns:
tracking error
"""
activeReturns = returns - benchmarkReturns
return np.std(activeReturns)
[docs]
def maxDrawdownDuration(returns):
"""
Maximum drawdown duration in periods.
Parameters:
returns: array of returns
Returns:
maximum drawdown duration
"""
cumReturns = np.cumsum(returns)
peak = np.maximum.accumulate(cumReturns)
underwater = cumReturns < peak
maxDuration = 0
currentDuration = 0
for isUnderwater in underwater:
if isUnderwater:
currentDuration += 1
maxDuration = max(maxDuration, currentDuration)
else:
currentDuration = 0
return maxDuration
[docs]
def valueAtRisk(returns, confidence=0.95, method='historical'):
"""
Value at Risk with method selection.
Parameters:
returns: array of returns
confidence: confidence level
method: 'historical' or 'parametric'
Returns:
VaR estimate
"""
if method == 'historical':
return historicalVar(returns, confidence)
elif method == 'parametric':
return parametricVar(returns, confidence)
else:
raise ValueError("method must be 'historical' or 'parametric'")
[docs]
def conditionalValueAtRisk(returns, confidence=0.95, method='historical'):
"""
Conditional Value at Risk with method selection.
Parameters:
returns: array of returns
confidence: confidence level
method: 'historical' or 'parametric'
Returns:
CVaR estimate
"""
if method == 'historical':
return historicalCvar(returns, confidence)
elif method == 'parametric':
return parametricCvar(returns, confidence)
else:
raise ValueError("method must be 'historical' or 'parametric'")
[docs]
def downsideDeviation(returns, target=0):
"""
Downside deviation.
Parameters:
returns: array of returns
target: target return
Returns:
downside deviation
"""
downsideReturns = returns[returns < target] - target
if len(downsideReturns) == 0:
return 0.0
return np.sqrt(np.mean(downsideReturns**2))
[docs]
def ulcerIndex(returns):
"""
Ulcer index (downside volatility measure).
Parameters:
returns: array of returns
Returns:
ulcer index
"""
cumReturns = np.cumsum(returns)
peak = np.maximum.accumulate(cumReturns)
drawdowns = (cumReturns - peak) / (peak + 1)
return np.sqrt(np.mean(drawdowns**2))
[docs]
def painIndex(returns):
"""
Pain index (average squared drawdown).
Parameters:
returns: array of returns
Returns:
pain index
"""
cumReturns = np.cumsum(returns)
peak = np.maximum.accumulate(cumReturns)
drawdowns = (cumReturns - peak) / (peak + 1)
return np.mean(drawdowns**2)
[docs]
def tailRatio(returns, confidence=0.95):
"""
Tail ratio: right tail / left tail.
Parameters:
returns: array of returns
confidence: confidence level
Returns:
tail ratio (>1 means right tail heavier)
"""
rightTail = np.percentile(returns, confidence * 100)
leftTail = np.percentile(returns, (1 - confidence) * 100)
return abs(rightTail / leftTail) if leftTail != 0 else np.inf
[docs]
def capturRatio(returns, marketReturns):
"""
Upside and downside capture ratios.
Parameters:
returns: portfolio returns
marketReturns: market returns
Returns:
dict with upsideCapture, downsideCapture, captureRatio
"""
upMarket = marketReturns > 0
downMarket = marketReturns < 0
if np.sum(upMarket) > 0:
upsideCapture = np.mean(returns[upMarket]) / np.mean(marketReturns[upMarket])
else:
upsideCapture = 0.0
if np.sum(downMarket) > 0:
downsideCapture = np.mean(returns[downMarket]) / np.mean(marketReturns[downMarket])
else:
downsideCapture = 0.0
captureRatio = upsideCapture / downsideCapture if downsideCapture != 0 else np.inf
return {
'upsideCapture': upsideCapture,
'downsideCapture': downsideCapture,
'captureRatio': captureRatio
}
[docs]
def stabilityRatio(returns):
"""
Stability of returns (R-squared of linear regression).
Parameters:
returns: array of returns
Returns:
stability ratio (0-1, higher = more stable)
"""
cumReturns = np.cumsum(returns)
x = np.arange(len(cumReturns))
xMean = np.mean(x)
yMean = np.mean(cumReturns)
numerator = np.sum((x - xMean) * (cumReturns - yMean))
denominator = np.sqrt(np.sum((x - xMean)**2) * np.sum((cumReturns - yMean)**2))
if denominator == 0:
return 0.0
correlation = numerator / denominator
return correlation**2