<< All versions
Skill v1.0.1
currentAutomated scan100/100ccf/claude-code-ccf-marketplace/risk-metrics-calculation
4 files
──Details
PublishedMay 15, 2026 at 05:06 AM
Content Hashsha256:1e37423ca4e5ec04...
Git SHA0cfb7e6e9748
Bump Typepatch
──Files
Files (1 file, 18.7 KB)
SKILL.md18.7 KBactive
SKILL.md · 559 lines · 18.7 KB
version: "1.0.1" name: risk-metrics-calculation description: Calculate portfolio risk metrics including VaR, CVaR, Sharpe, Sortino, and drawdown analysis. Use when measuring portfolio risk, implementing risk limits, or building risk monitoring systems.
Risk Metrics Calculation
Comprehensive risk measurement toolkit for portfolio management, including Value at Risk, Expected Shortfall, and drawdown analysis.
When to Use This Skill
- Measuring portfolio risk
- Implementing risk limits
- Building risk dashboards
- Calculating risk-adjusted returns
- Setting position sizes
- Regulatory reporting
Core Concepts
1. Risk Metric Categories
| Category | Metrics | Use Case | |
|---|---|---|---|
| Volatility | Std Dev, Beta | General risk | |
| Tail Risk | VaR, CVaR | Extreme losses | |
| Drawdown | Max DD, Calmar | Capital preservation | |
| Risk-Adjusted | Sharpe, Sortino | Performance |
2. Time Horizons
Intraday: Minute/hourly VaR for day tradersDaily: Standard risk reportingWeekly: Rebalancing decisionsMonthly: Performance attributionAnnual: Strategic allocation
Implementation
Pattern 1: Core Risk Metrics
python
import numpy as npimport pandas as pdfrom scipy import statsfrom typing import Dict, Optional, Tupleclass RiskMetrics:"""Core risk metric calculations."""def __init__(self, returns: pd.Series, rf_rate: float = 0.02):"""Args:returns: Series of periodic returnsrf_rate: Annual risk-free rate"""self.returns = returnsself.rf_rate = rf_rateself.ann_factor = 252 # Trading days per year# Volatility Metricsdef volatility(self, annualized: bool = True) -> float:"""Standard deviation of returns."""vol = self.returns.std()if annualized:vol *= np.sqrt(self.ann_factor)return voldef downside_deviation(self, threshold: float = 0, annualized: bool = True) -> float:"""Standard deviation of returns below threshold."""downside = self.returns[self.returns < threshold]if len(downside) == 0:return 0.0dd = downside.std()if annualized:dd *= np.sqrt(self.ann_factor)return dddef beta(self, market_returns: pd.Series) -> float:"""Beta relative to market."""aligned = pd.concat([self.returns, market_returns], axis=1).dropna()if len(aligned) < 2:return np.nancov = np.cov(aligned.iloc[:, 0], aligned.iloc[:, 1])return cov[0, 1] / cov[1, 1] if cov[1, 1] != 0 else 0# Value at Riskdef var_historical(self, confidence: float = 0.95) -> float:"""Historical VaR at confidence level."""return -np.percentile(self.returns, (1 - confidence) * 100)def var_parametric(self, confidence: float = 0.95) -> float:"""Parametric VaR assuming normal distribution."""z_score = stats.norm.ppf(1 - confidence) # Lower tail (5th percentile for 95% conf)return -(self.returns.mean() + z_score * self.returns.std())def var_cornish_fisher(self, confidence: float = 0.95) -> float:"""VaR with Cornish-Fisher expansion for non-normality."""z = stats.norm.ppf(1 - confidence) # Lower tail z-scores = stats.skew(self.returns) # Skewnessk = stats.kurtosis(self.returns) # Excess kurtosis# Cornish-Fisher expansionz_cf = (z + (z**2 - 1) * s / 6 +(z**3 - 3*z) * k / 24 -(2*z**3 - 5*z) * s**2 / 36)return -(self.returns.mean() + z_cf * self.returns.std())# Conditional VaR (Expected Shortfall)def cvar(self, confidence: float = 0.95) -> float:"""Expected Shortfall / CVaR / Average VaR."""var = self.var_historical(confidence)return -self.returns[self.returns <= -var].mean()# Drawdown Analysisdef drawdowns(self) -> pd.Series:"""Calculate drawdown series."""cumulative = (1 + self.returns).cumprod()running_max = cumulative.cummax()return (cumulative - running_max) / running_maxdef max_drawdown(self) -> float:"""Maximum drawdown."""return self.drawdowns().min()def avg_drawdown(self) -> float:"""Average drawdown."""dd = self.drawdowns()return dd[dd < 0].mean() if (dd < 0).any() else 0def drawdown_duration(self) -> Dict[str, int]:"""Drawdown duration statistics."""dd = self.drawdowns()in_drawdown = dd < 0# Find drawdown periodsdrawdown_starts = in_drawdown & ~in_drawdown.shift(1).fillna(False)drawdown_ends = ~in_drawdown & in_drawdown.shift(1).fillna(False)durations = []current_duration = 0for i in range(len(dd)):if in_drawdown.iloc[i]:current_duration += 1elif current_duration > 0:durations.append(current_duration)current_duration = 0if current_duration > 0:durations.append(current_duration)return {"max_duration": max(durations) if durations else 0,"avg_duration": np.mean(durations) if durations else 0,"current_duration": current_duration}# Risk-Adjusted Returnsdef sharpe_ratio(self) -> float:"""Annualized Sharpe ratio."""excess_return = self.returns.mean() * self.ann_factor - self.rf_ratevol = self.volatility(annualized=True)return excess_return / vol if vol > 0 else 0def sortino_ratio(self) -> float:"""Sortino ratio using downside deviation."""excess_return = self.returns.mean() * self.ann_factor - self.rf_ratedd = self.downside_deviation(threshold=0, annualized=True)return excess_return / dd if dd > 0 else 0def calmar_ratio(self) -> float:"""Calmar ratio (return / max drawdown)."""annual_return = (1 + self.returns).prod() ** (self.ann_factor / len(self.returns)) - 1max_dd = abs(self.max_drawdown())return annual_return / max_dd if max_dd > 0 else 0def omega_ratio(self, threshold: float = 0) -> float:"""Omega ratio."""returns_above = self.returns[self.returns > threshold] - thresholdreturns_below = threshold - self.returns[self.returns <= threshold]if returns_below.sum() == 0:return np.infreturn returns_above.sum() / returns_below.sum()# Information Ratiodef information_ratio(self, benchmark_returns: pd.Series) -> float:"""Information ratio vs benchmark."""active_returns = self.returns - benchmark_returnstracking_error = active_returns.std() * np.sqrt(self.ann_factor)active_return = active_returns.mean() * self.ann_factorreturn active_return / tracking_error if tracking_error > 0 else 0# Summarydef summary(self) -> Dict[str, float]:"""Generate comprehensive risk summary."""dd_stats = self.drawdown_duration()return {# Returns"total_return": (1 + self.returns).prod() - 1,"annual_return": (1 + self.returns).prod() ** (self.ann_factor / len(self.returns)) - 1,# Volatility"annual_volatility": self.volatility(),"downside_deviation": self.downside_deviation(),# VaR & CVaR"var_95_historical": self.var_historical(0.95),"var_99_historical": self.var_historical(0.99),"cvar_95": self.cvar(0.95),# Drawdowns"max_drawdown": self.max_drawdown(),"avg_drawdown": self.avg_drawdown(),"max_drawdown_duration": dd_stats["max_duration"],# Risk-Adjusted"sharpe_ratio": self.sharpe_ratio(),"sortino_ratio": self.sortino_ratio(),"calmar_ratio": self.calmar_ratio(),"omega_ratio": self.omega_ratio(),# Distribution"skewness": stats.skew(self.returns),"kurtosis": stats.kurtosis(self.returns),}
Pattern 2: Portfolio Risk
python
class PortfolioRisk:"""Portfolio-level risk calculations."""def __init__(self,returns: pd.DataFrame,weights: Optional[pd.Series] = None):"""Args:returns: DataFrame with asset returns (columns = assets)weights: Portfolio weights (default: equal weight)"""self.returns = returnsself.weights = weights if weights is not None else \pd.Series(1/len(returns.columns), index=returns.columns)self.ann_factor = 252def portfolio_return(self) -> float:"""Weighted portfolio return."""return (self.returns @ self.weights).mean() * self.ann_factordef portfolio_volatility(self) -> float:"""Portfolio volatility."""cov_matrix = self.returns.cov() * self.ann_factorport_var = self.weights @ cov_matrix @ self.weightsreturn np.sqrt(port_var)def marginal_risk_contribution(self) -> pd.Series:"""Marginal contribution to risk by asset."""cov_matrix = self.returns.cov() * self.ann_factorport_vol = self.portfolio_volatility()# Marginal contributionmrc = (cov_matrix @ self.weights) / port_volreturn mrcdef component_risk(self) -> pd.Series:"""Component contribution to total risk."""mrc = self.marginal_risk_contribution()return self.weights * mrcdef risk_parity_weights(self, target_vol: float = None) -> pd.Series:"""Calculate risk parity weights."""from scipy.optimize import minimizen = len(self.returns.columns)cov_matrix = self.returns.cov() * self.ann_factordef risk_budget_objective(weights):port_vol = np.sqrt(weights @ cov_matrix @ weights)mrc = (cov_matrix @ weights) / port_volrc = weights * mrctarget_rc = port_vol / n # Equal risk contributionreturn np.sum((rc - target_rc) ** 2)constraints = [{"type": "eq", "fun": lambda w: np.sum(w) - 1}, # Weights sum to 1]bounds = [(0.01, 1.0) for _ in range(n)] # Min 1%, max 100%x0 = np.array([1/n] * n)result = minimize(risk_budget_objective,x0,method="SLSQP",bounds=bounds,constraints=constraints)return pd.Series(result.x, index=self.returns.columns)def correlation_matrix(self) -> pd.DataFrame:"""Asset correlation matrix."""return self.returns.corr()def diversification_ratio(self) -> float:"""Diversification ratio (higher = more diversified)."""asset_vols = self.returns.std() * np.sqrt(self.ann_factor)weighted_vol = (self.weights * asset_vols).sum()port_vol = self.portfolio_volatility()return weighted_vol / port_vol if port_vol > 0 else 1def tracking_error(self, benchmark_returns: pd.Series) -> float:"""Tracking error vs benchmark."""port_returns = self.returns @ self.weightsactive_returns = port_returns - benchmark_returnsreturn active_returns.std() * np.sqrt(self.ann_factor)def conditional_correlation(self,threshold_percentile: float = 10) -> pd.DataFrame:"""Correlation during stress periods."""port_returns = self.returns @ self.weightsthreshold = np.percentile(port_returns, threshold_percentile)stress_mask = port_returns <= thresholdreturn self.returns[stress_mask].corr()
Pattern 3: Rolling Risk Metrics
python
class RollingRiskMetrics:"""Rolling window risk calculations."""def __init__(self, returns: pd.Series, window: int = 63):"""Args:returns: Return serieswindow: Rolling window size (default: 63 = ~3 months)"""self.returns = returnsself.window = windowdef rolling_volatility(self, annualized: bool = True) -> pd.Series:"""Rolling volatility."""vol = self.returns.rolling(self.window).std()if annualized:vol *= np.sqrt(252)return voldef rolling_sharpe(self, rf_rate: float = 0.02) -> pd.Series:"""Rolling Sharpe ratio."""rolling_return = self.returns.rolling(self.window).mean() * 252rolling_vol = self.rolling_volatility()return (rolling_return - rf_rate) / rolling_voldef rolling_var(self, confidence: float = 0.95) -> pd.Series:"""Rolling historical VaR."""return self.returns.rolling(self.window).apply(lambda x: -np.percentile(x, (1 - confidence) * 100),raw=True)def rolling_max_drawdown(self) -> pd.Series:"""Rolling maximum drawdown."""def max_dd(returns):cumulative = (1 + returns).cumprod()running_max = cumulative.cummax()drawdowns = (cumulative - running_max) / running_maxreturn drawdowns.min()return self.returns.rolling(self.window).apply(max_dd, raw=False)def rolling_beta(self, market_returns: pd.Series) -> pd.Series:"""Rolling beta vs market."""def calc_beta(window_data):port_ret = window_data.iloc[:, 0]mkt_ret = window_data.iloc[:, 1]cov = np.cov(port_ret, mkt_ret)return cov[0, 1] / cov[1, 1] if cov[1, 1] != 0 else 0combined = pd.concat([self.returns, market_returns], axis=1)return combined.rolling(self.window).apply(lambda x: calc_beta(x.to_frame()),raw=False).iloc[:, 0]def volatility_regime(self,low_threshold: float = 0.10,high_threshold: float = 0.20) -> pd.Series:"""Classify volatility regime."""vol = self.rolling_volatility()def classify(v):if v < low_threshold:return "low"elif v > high_threshold:return "high"else:return "normal"return vol.apply(classify)
Pattern 4: Stress Testing
python
class StressTester:"""Historical and hypothetical stress testing."""# Historical crisis periodsHISTORICAL_SCENARIOS = {"2008_financial_crisis": ("2008-09-01", "2009-03-31"),"2020_covid_crash": ("2020-02-19", "2020-03-23"),"2022_rate_hikes": ("2022-01-01", "2022-10-31"),"dot_com_bust": ("2000-03-01", "2002-10-01"),"flash_crash_2010": ("2010-05-06", "2010-05-06"),}def __init__(self, returns: pd.Series, weights: pd.Series = None):self.returns = returnsself.weights = weightsdef historical_stress_test(self,scenario_name: str,historical_data: pd.DataFrame) -> Dict[str, float]:"""Test portfolio against historical crisis period."""if scenario_name not in self.HISTORICAL_SCENARIOS:raise ValueError(f"Unknown scenario: {scenario_name}")start, end = self.HISTORICAL_SCENARIOS[scenario_name]# Get returns during crisiscrisis_returns = historical_data.loc[start:end]if self.weights is not None:port_returns = (crisis_returns @ self.weights)else:port_returns = crisis_returnstotal_return = (1 + port_returns).prod() - 1max_dd = self._calculate_max_dd(port_returns)worst_day = port_returns.min()return {"scenario": scenario_name,"period": f"{start} to {end}","total_return": total_return,"max_drawdown": max_dd,"worst_day": worst_day,"volatility": port_returns.std() * np.sqrt(252)}def hypothetical_stress_test(self,shocks: Dict[str, float]) -> float:"""Test portfolio against hypothetical shocks.Args:shocks: Dict of {asset: shock_return}"""if self.weights is None:raise ValueError("Weights required for hypothetical stress test")total_impact = 0for asset, shock in shocks.items():if asset in self.weights.index:total_impact += self.weights[asset] * shockreturn total_impactdef monte_carlo_stress(self,n_simulations: int = 10000,horizon_days: int = 21,vol_multiplier: float = 2.0) -> Dict[str, float]:"""Monte Carlo stress test with elevated volatility."""mean = self.returns.mean()vol = self.returns.std() * vol_multipliersimulations = np.random.normal(mean,vol,(n_simulations, horizon_days))total_returns = (1 + simulations).prod(axis=1) - 1return {"expected_loss": -total_returns.mean(),"var_95": -np.percentile(total_returns, 5),"var_99": -np.percentile(total_returns, 1),"worst_case": -total_returns.min(),"prob_10pct_loss": (total_returns < -0.10).mean()}def _calculate_max_dd(self, returns: pd.Series) -> float:cumulative = (1 + returns).cumprod()running_max = cumulative.cummax()drawdowns = (cumulative - running_max) / running_maxreturn drawdowns.min()
Quick Reference
python
# Daily usagemetrics = RiskMetrics(returns)print(f"Sharpe: {metrics.sharpe_ratio():.2f}")print(f"Max DD: {metrics.max_drawdown():.2%}")print(f"VaR 95%: {metrics.var_historical(0.95):.2%}")# Full summarysummary = metrics.summary()for metric, value in summary.items():print(f"{metric}: {value:.4f}")
Best Practices
Do's
- Use multiple metrics - No single metric captures all risk
- Consider tail risk - VaR isn't enough, use CVaR
- Rolling analysis - Risk changes over time
- Stress test - Historical and hypothetical
- Document assumptions - Distribution, lookback, etc.
Don'ts
- Don't rely on VaR alone - Underestimates tail risk
- Don't assume normality - Returns are fat-tailed
- Don't ignore correlation - Increases in stress
- Don't use short lookbacks - Miss regime changes
- Don't forget transaction costs - Affects realized risk