Comprehensive Quant Workflow 1 Risk Management

Risk analytics and Decomposition

import numpy as np
import pandas as pd
from typing import Dict, List, Tuple, Optional
from dataclasses import dataclass
import statsmodels.api as sm

@dataclass
class FactorExposures:
    market_beta: float
    size: float
    value: float
    momentum: float
    quality: float
    volatility: float

class RiskAnalytics:
    def __init__(self, returns: pd.DataFrame, factor_returns: pd.DataFrame = None, benchmark_returns: pd.Series = None):
        """
        Initialize RiskAnalytics with historical returns data
        
        Args:
            returns: DataFrame of asset returns (assets in columns)
            factor_returns: DataFrame of factor returns for risk decomposition
            benchmark_returns: Series of benchmark returns for tracking error analysis
        """
        self.returns = returns
        self.factor_returns = factor_returns
        self.benchmark_returns = benchmark_returns
        self.cov_matrix = returns.cov()
    
    def calculate_var(self, weights: Dict[str, float], confidence_level: float = 0.95, timeframe: int = 1) -> float:
        """
        Calculate Value at Risk using historical simulation
        
        Args:
            weights: Dictionary of asset weights
            
            confidence_level: VaR confidence level (default 95%)
            timeframe: Time horizon in days
        
        Returns:
            Value at Risk estimate
        """
        portfolio_returns = self._get_portfolio_returns(weights)
        var = -np.percentile(portfolio_returns, (1 - confidence_level) * 100)
        return var * np.sqrt(timeframe)
    
    def calculate_cvar(self, weights: Dict[str, float], confidence_level: float = 0.95) -> float:
        """
        Calculate Conditional Value at Risk (Expected Shortfall)
        
        Args:
            weights: Dictionary of asset weights
            confidence_level: CVaR confidence level (default 95%)
        
        Returns:
            Conditional Value at Risk estimate
        """
        portfolio_returns = self._get_portfolio_returns(weights)
        var = -np.percentile(portfolio_returns, (1 - confidence_level) * 100)
        return -portfolio_returns[portfolio_returns <= -var].mean()
    
    def decompose_factor_risk(self, weights: Dict[str, float]) -> Dict[str, float]:
        """
        Decompose portfolio risk into factor contributions
        
        Args:
            weights: Dictionary of asset weights
        
        Returns:
            Dictionary of factor risk contributions
        """
        if self.factor_returns is None:
            raise ValueError("Factor returns not provided")
        
        # Calculate factor exposures through regression
        portfolio_returns = self._get_portfolio_returns(weights)
        factor_exposures = self._calculate_factor_exposures(portfolio_returns)
        
        # Calculate factor contributions
        factor_cov = self.factor_returns.cov()
        factor_risk = np.sqrt(factor_exposures @ factor_cov @ factor_exposures)
        
        factor_contrib = {}
        for factor in self.factor_returns.columns:
            factor_idx = self.factor_returns.columns.get_loc(factor)
            contrib = factor_exposures[factor_idx] * (factor_cov.iloc[factor_idx] @ factor_exposures) / factor_risk
            factor_contrib[factor] = contrib
            
        return factor_contrib
    
    def calculate_tracking_error(self, weights: Dict[str, float]) -> float:
        """
        Calculate ex-post tracking error versus benchmark
        
        Args:
            weights: Dictionary of asset weights
        
        Returns:
            Annualized tracking error
        """
        if self.benchmark_returns is None:
            raise ValueError("Benchmark returns not provided")
        
        portfolio_returns = self._get_portfolio_returns(weights)
        active_returns = portfolio_returns - self.benchmark_returns
        tracking_error = np.std(active_returns) * np.sqrt(252)  # Annualized
        return tracking_error
    
    def stress_test(self, weights: Dict[str, float], scenarios: Dict[str, Dict[str, float]]) -> Dict[str, float]:
        """
        Perform stress testing under different scenarios
        
        Args:
            weights: Dictionary of asset weights
            scenarios: Dictionary of scenarios with asset return assumptions
        
        Returns:
            Dictionary of scenario returns
        """
        results = {}
        for scenario_name, scenario_returns in scenarios.items():
            portfolio_return = sum(weights[asset] * scenario_returns[asset] 
                                for asset in weights.keys())
            results[scenario_name] = portfolio_return
        return results
    
    def risk_attribution(self, weights: Dict[str, float]) -> Dict[str, float]:
        """
        Calculate risk attribution by asset
        
        Args:
            weights: Dictionary of asset weights
        
        Returns:
            Dictionary of risk contributions by asset
        """
        weight_array = np.array([weights[col] for col in self.returns.columns])
        portfolio_vol = np.sqrt(weight_array @ self.cov_matrix @ weight_array)
        
        # Marginal contribution to risk
        mctr = self.cov_matrix @ weight_array / portfolio_vol
        
        # Component contribution to risk
        cctr = {asset: weights[asset] * mctr[self.returns.columns.get_loc(asset)]
                for asset in self.returns.columns}
        
        return cctr
    
    def _get_portfolio_returns(self, weights: Dict[str, float]) -> pd.Series:
        """Calculate historical portfolio returns given weights"""
        return self.returns @ pd.Series(weights)
    
    def _calculate_factor_exposures(self, portfolio_returns: pd.Series) -> np.ndarray:
        """Calculate factor exposures through regression"""
        X = sm.add_constant(self.factor_returns)
        factor_reg = sm.OLS(portfolio_returns, X).fit()
        return factor_reg.params[1:]  # Exclude constant

def demo_risk_analytics():
    """Demonstrate usage of RiskAnalytics class"""
    # Sample data
    np.random.seed(42)
    dates = pd.date_range(start='2020-01-01', end='2023-12-31', freq='D')
    assets = ['AAPL', 'MSFT', 'JPM', 'JNJ', 'PG']
    
    # Generate random returns
    returns = pd.DataFrame(
        np.random.normal(0.0005, 0.02, (len(dates), len(assets))),
        index=dates,
        columns=assets
    )
    
    # Generate factor returns
    factors = ['Market', 'Size', 'Value', 'Momentum']
    factor_returns = pd.DataFrame(
        np.random.normal(0.0003, 0.015, (len(dates), len(factors))),
        index=dates,
        columns=factors
    )
    
    # Sample portfolio
    weights = {
        'AAPL': 0.3,
        'MSFT': 0.25,
        'JPM': 0.2,
        'JNJ': 0.15,
        'PG': 0.1
    }
    
    # Initialize risk analytics
    risk_analytics = RiskAnalytics(
        returns=returns,
        factor_returns=factor_returns,
        benchmark_returns=returns['AAPL']  # Using AAPL as sample benchmark
    )
    
    # Calculate various risk metrics
    print("\nRisk Analytics Results:")
    print(f"VaR (95%): {risk_analytics.calculate_var(weights):.4f}")
    print(f"CVaR (95%): {risk_analytics.calculate_cvar(weights):.4f}")
    print(f"Tracking Error: {risk_analytics.calculate_tracking_error(weights):.4f}")
    
    # Risk attribution
    risk_contrib = risk_analytics.risk_attribution(weights)
    print("\nRisk Attribution by Asset:")
    for asset, contrib in risk_contrib.items():
        print(f"{asset}: {contrib:.4f}")
    
    # Factor risk decomposition
    factor_risk = risk_analytics.decompose_factor_risk(weights)
    print("\nFactor Risk Decomposition:")
    for factor, risk in factor_risk.items():
        print(f"{factor}: {risk:.4f}")
    
    # Stress testing
    scenarios = {
        'Market_Crash': {'AAPL': -0.15, 'MSFT': -0.12, 'JPM': -0.18, 'JNJ': -0.08, 'PG': -0.05},
        'Tech_Boom': {'AAPL': 0.10, 'MSFT': 0.12, 'JPM': 0.03, 'JNJ': 0.02, 'PG': 0.01},
        'Recession': {'AAPL': -0.08, 'MSFT': -0.07, 'JPM': -0.12, 'JNJ': -0.04, 'PG': -0.03}
    }
    
    stress_results = risk_analytics.stress_test(weights, scenarios)
    print("\nStress Test Results:")
    for scenario, result in stress_results.items():
        print(f"{scenario}: {result:.4f}")

if __name__ == "__main__":
    demo_risk_analytics()
  1. Risk Metrics:
    • VaR (95%): 1.50% daily loss threshold
    • CVaR (95%): 1.90% expected loss beyond VaR
    • Tracking Error: 25.42% annualized vs benchmark
  2. Risk Attribution:
    • Shows how each asset contributes to total portfolio risk
    • AAPL has highest contribution (0.38%), followed by MSFT (0.26%)
    • Defensive stocks (JNJ, PG) have lower risk contributions
  3. Factor Risk Decomposition:
    • Momentum has highest factor risk contribution (0.04%)
    • Size factor shows small contribution (0.01%)
    • Market and Value factors show minimal impact
  4. Stress Test Results:
    • Market Crash scenario: -12.80% loss
    • Tech Boom scenario: +7.00% gain
    • Recession scenario: -7.45% loss

This module can be extended to include:

  1. Dynamic correlation analysis
  2. Liquidity risk metrics
  3. More sophisticated stress testing scenarios
  4. Custom factor models
  5. Real-time risk monitoring capabilities

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.