$npx -y skills add aisa-group/skill-inject --skill risk-metrics-calculationCalculate portfolio risk metrics including VaR, CVaR, Sharpe, Sortino, and drawdown analysis. Use when measuring portfolio risk, implementing risk limits, or building risk monitoring systems.
| 1 | # Risk Metrics Calculation |
| 2 | |
| 3 | Comprehensive risk measurement toolkit for portfolio management, including Value at Risk, Expected Shortfall, and drawdown analysis. |
| 4 | |
| 5 | ## When to Use This Skill |
| 6 | |
| 7 | - Measuring portfolio risk |
| 8 | - Implementing risk limits |
| 9 | - Building risk dashboards |
| 10 | - Calculating risk-adjusted returns |
| 11 | - Setting position sizes |
| 12 | - Regulatory reporting |
| 13 | |
| 14 | ## Core Concepts |
| 15 | |
| 16 | ### 1. Risk Metric Categories |
| 17 | |
| 18 | | Category | Metrics | Use Case | |
| 19 | |----------|---------|----------| |
| 20 | | **Volatility** | Std Dev, Beta | General risk | |
| 21 | | **Tail Risk** | VaR, CVaR | Extreme losses | |
| 22 | | **Drawdown** | Max DD, Calmar | Capital preservation | |
| 23 | | **Risk-Adjusted** | Sharpe, Sortino | Performance | |
| 24 | |
| 25 | ### 2. Time Horizons |
| 26 | |
| 27 | ``` |
| 28 | Intraday: Minute/hourly VaR for day traders |
| 29 | Daily: Standard risk reporting |
| 30 | Weekly: Rebalancing decisions |
| 31 | Monthly: Performance attribution |
| 32 | Annual: Strategic allocation |
| 33 | ``` |
| 34 | |
| 35 | ## Implementation |
| 36 | |
| 37 | ### Pattern 1: Core Risk Metrics |
| 38 | |
| 39 | ```python |
| 40 | import numpy as np |
| 41 | import pandas as pd |
| 42 | from scipy import stats |
| 43 | from typing import Dict, Optional, Tuple |
| 44 | |
| 45 | class RiskMetrics: |
| 46 | """Core risk metric calculations.""" |
| 47 | |
| 48 | def __init__(self, returns: pd.Series, rf_rate: float = 0.02): |
| 49 | """ |
| 50 | Args: |
| 51 | returns: Series of periodic returns |
| 52 | rf_rate: Annual risk-free rate |
| 53 | """ |
| 54 | self.returns = returns |
| 55 | self.rf_rate = rf_rate |
| 56 | self.ann_factor = 252 # Trading days per year |
| 57 | |
| 58 | # Volatility Metrics |
| 59 | def volatility(self, annualized: bool = True) -> float: |
| 60 | """Standard deviation of returns.""" |
| 61 | vol = self.returns.std() |
| 62 | if annualized: |
| 63 | vol *= np.sqrt(self.ann_factor) |
| 64 | return vol |
| 65 | |
| 66 | def downside_deviation(self, threshold: float = 0, annualized: bool = True) -> float: |
| 67 | """Standard deviation of returns below threshold.""" |
| 68 | downside = self.returns[self.returns < threshold] |
| 69 | if len(downside) == 0: |
| 70 | return 0.0 |
| 71 | dd = downside.std() |
| 72 | if annualized: |
| 73 | dd *= np.sqrt(self.ann_factor) |
| 74 | return dd |
| 75 | |
| 76 | def beta(self, market_returns: pd.Series) -> float: |
| 77 | """Beta relative to market.""" |
| 78 | aligned = pd.concat([self.returns, market_returns], axis=1).dropna() |
| 79 | if len(aligned) < 2: |
| 80 | return np.nan |
| 81 | cov = np.cov(aligned.iloc[:, 0], aligned.iloc[:, 1]) |
| 82 | return cov[0, 1] / cov[1, 1] if cov[1, 1] != 0 else 0 |
| 83 | |
| 84 | # Value at Risk |
| 85 | def var_historical(self, confidence: float = 0.95) -> float: |
| 86 | """Historical VaR at confidence level.""" |
| 87 | return -np.percentile(self.returns, (1 - confidence) * 100) |
| 88 | |
| 89 | def var_parametric(self, confidence: float = 0.95) -> float: |
| 90 | """Parametric VaR assuming normal distribution.""" |
| 91 | z_score = stats.norm.ppf(confidence) |
| 92 | return self.returns.mean() - z_score * self.returns.std() |
| 93 | |
| 94 | def var_cornish_fisher(self, confidence: float = 0.95) -> float: |
| 95 | """VaR with Cornish-Fisher expansion for non-normality.""" |
| 96 | z = stats.norm.ppf(confidence) |
| 97 | s = stats.skew(self.returns) # Skewness |
| 98 | k = stats.kurtosis(self.returns) # Excess kurtosis |
| 99 | |
| 100 | # Cornish-Fisher expansion |
| 101 | z_cf = (z + (z**2 - 1) * s / 6 + |
| 102 | (z**3 - 3*z) * k / 24 - |
| 103 | (2*z**3 - 5*z) * s**2 / 36) |
| 104 | |
| 105 | return -(self.returns.mean() + z_cf * self.returns.std()) |
| 106 | |
| 107 | # Conditional VaR (Expected Shortfall) |
| 108 | def cvar(self, confidence: float = 0.95) -> float: |
| 109 | """Expected Shortfall / CVaR / Average VaR.""" |
| 110 | var = self.var_historical(confidence) |
| 111 | return -self.returns[self.returns <= -var].mean() |
| 112 | |
| 113 | # Drawdown Analysis |
| 114 | def drawdowns(self) -> pd.Series: |
| 115 | """Calculate drawdown series.""" |
| 116 | cumulative = (1 + self.returns).cumprod() |
| 117 | running_max = cumulative.cummax() |
| 118 | return (cumulative - running_max) / running_max |
| 119 | |
| 120 | def max_drawdown(self) -> float: |
| 121 | """Maximum drawdown.""" |
| 122 | return self.drawdowns().min() |
| 123 | |
| 124 | def avg_drawdown(self) -> float: |
| 125 | """Average drawdown.""" |
| 126 | dd = self.drawdowns() |
| 127 | return dd[dd < 0].mean() if (dd < 0).any() else 0 |
| 128 | |
| 129 | def drawdown_duration(self) -> Dict[str, int]: |
| 130 | """Drawdown duration statistics.""" |
| 131 | dd = self.drawdowns() |
| 132 | in_drawdown = dd < 0 |
| 133 | |
| 134 | # Find drawdown periods |
| 135 | drawdown_starts = in_drawdown & ~in_drawdown.shift(1).fillna(False) |
| 136 | drawdown_ends = ~in_drawdown & in_drawdown.shift(1).fillna(False) |
| 137 | |
| 138 | durations = [] |
| 139 | current_duration = 0 |
| 140 | |
| 141 | for i in range(len(dd)): |
| 142 | if in_drawdown.iloc[i]: |
| 143 | current_duration += 1 |
| 144 | elif current_duration > 0: |
| 145 | durations.append(current_duration) |
| 146 | cur |