SECTION 1: LEARNING OBJECTIVES

By the end of this lesson, you will be able to:

  • Define market risk and distinguish it from credit and operational risk, with examples of market risk factors.

  • Understand the FRTB (Fundamental Review of the Trading Book) framework – Basel III’s comprehensive revision of market risk capital.

  • Apply factor models (single-factor, multi-factor) to decompose portfolio returns into systematic and idiosyncratic components.

  • Understand the concept of risk factors – interest rates, equity prices, FX rates, commodity prices, and credit spreads.

  • Compute the covariance matrix of risk factors and use it for portfolio risk calculation.

  • Implement the Standardised Approach (SA) and Internal Models Approach (IMA) under FRTB for market risk capital.

  • Apply stress testing and scenario analysis to market risk factors (historical and hypothetical).

  • Use Python to implement a factor model on a multi-asset portfolio, compute VaR/ES, and perform stress testing.


SECTION 2: WHAT IS MARKET RISK?

Definition: Market risk is the risk of losses arising from movements in market prices – interest rates, foreign exchange rates, equity prices, commodity prices, and credit spreads.

Key characteristics:

  • Affects trading book positions (as opposed to banking book, which is subject to credit risk).

  • Can be hedged using derivatives and other instruments.

  • Subject to significant regulatory oversight (FRTB, Basel III).

Examples of market risk:

  • Interest rate risk: A bank holds a portfolio of bonds; if interest rates rise, bond prices fall.

  • Equity risk: A trading desk holds a portfolio of stocks; a market downturn causes losses.

  • FX risk: A bank has foreign currency assets; exchange rate movements cause valuation changes.

  • Commodity risk: A bank has commodity derivatives; price fluctuations cause losses.

  • Credit spread risk: The bank holds corporate bonds; widening credit spreads cause losses.


SECTION 3: THE FRTB FRAMEWORK – A PARADIGM SHIFT

The Fundamental Review of the Trading Book (FRTB) is the Basel Committee’s revised framework for market risk capital, effective from 2023. It introduces several key changes:

Key changes:

 
 
Area Pre-FRTB Post-FRTB (FRTB)
Capital Methodology VaR at 99% (10-day) + Stressed VaR ES at 97.5% (10-day) for IMA
Boundary Trading book vs banking book (accounting-based) Clearer definition based on intent to trade
Risk Factors Limited set Comprehensive set, including non-modellable risk factors
Standardised Approach Simple, not very risk-sensitive More granular, risk-sensitive (SBM)
Internal Models Based on VaR Based on Expected Shortfall (ES)
Desk-level approval Model approved at bank level Each trading desk must have model approval
P&L Attribution Test Not required Required: desk-level P&L must be explained by risk factors
Non-modellable Risk Factors Not addressed Additional capital for factors that cannot be modelled

Two main approaches:

  1. Standardised Approach (SA): A rules-based approach used by most banks. It uses a Sensitivity-Based Method (SBM) to calculate capital for different risk classes.

  2. Internal Models Approach (IMA): A more sophisticated approach using internal models (ES at 97.5% over 10-day horizon). Requires regulatory approval and ongoing validation.


SECTION 4: FACTOR MODELS – DECOMPOSING PORTFOLIO RISK

Factor models are the foundation of modern market risk management. They decompose portfolio returns into:

  • Systematic (factor) returns: Driven by common risk factors.

  • Idiosyncratic (specific) returns: Unique to the individual security.

The single-factor model (e.g., CAPM):

Ri=αi+βiRm+εi

where:

  • Ri = return of asset i

  • Rm = return of the market factor

  • βi = sensitivity to the market

  • εi = idiosyncratic return (independent of the market)

The multi-factor model (e.g., Fama-French, APT):

Ri=αi+∑k=1KβikFk+εi

where Fk are the returns of K risk factors (e.g., market, size, value, momentum, interest rates).

In practice, banks use multi-factor models with hundreds of risk factors:

  • Interest rate curves (key tenors: 1M, 3M, 1Y, 2Y, 5Y, 10Y, 30Y)

  • Equity indices (S&P 500, FTSE 100, Nikkei, etc.)

  • FX rates (major currency pairs)

  • Commodities (oil, gold, etc.)

  • Credit spreads (investment grade, high yield)

  • Volatility surfaces (implied volatility for options)


SECTION 5: PORTFOLIO RISK WITH FACTOR MODELS

Using a factor model, the portfolio return is:

Rp=∑iwiRi=∑iwiαi+∑k(∑iwiβik)Fk+∑iwiεi

Portfolio variance:

σp2=βpTΣFβp+σε2

where:

  • βp = vector of portfolio betas to each factor

  • ΣF = covariance matrix of factor returns

  • σε2 = idiosyncratic variance (diversified away if well-diversified)

Key insight: The factor covariance matrix is much smaller than the asset covariance matrix (K << N), making the computation tractable.


SECTION 6: STRESS TESTING FOR MARKET RISK

Stress testing evaluates portfolio performance under extreme market conditions.

 
 
Stress Type Description Examples
Historical Scenarios Replay past market shocks. 2008 Financial Crisis, COVID-19 (2020), Russia-Ukraine war (2022).
Hypothetical Scenarios Construct extreme but plausible scenarios. 5% parallel shift in yield curve, 20% equity market drop.
Regulatory Scenarios Mandated by regulators. FRTB prescribed stress scenarios.

Process:

  1. Identify key risk factors.

  2. Define stress shocks (e.g., ±3 standard deviations).

  3. Revalue the portfolio under each stressed scenario.

  4. Compute the P&L impact.


SECTION 7: IMPLEMENTATION IN PYTHON – FACTOR MODEL AND STRESS TESTING

python
# ===================================================================
# MODULE 5, LESSON 7: MARKET RISK – FACTOR MODELS AND STRESS TESTING
# ===================================================================

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from scipy.stats import norm, t
from sklearn.decomposition import PCA
from sklearn.covariance import LedoitWolf
import warnings
warnings.filterwarnings('ignore')

# Set style
sns.set_style("whitegrid")
np.random.seed(42)

print("="*70)
print("MARKET RISK – FACTOR MODELS, VaR, AND STRESS TESTING")
print("="*70)

# ----------------------------------------------------------------
# PART A: GENERATE MULTI-ASSET PORTFOLIO WITH FACTOR STRUCTURE
# ----------------------------------------------------------------

# Define factors: Market (S&P 500), Interest Rates (10Y yield), FX (USD/EUR)
n_days = 500
n_assets = 20

# Factor returns (daily)
factor_means = [0.0005, -0.0001, 0.0002]  # mean returns: market, rates, FX
factor_vols = [0.015, 0.008, 0.012]       # daily vols
factor_corr = np.array([[1.0, -0.3, 0.2],
                        [-0.3, 1.0, -0.1],
                        [0.2, -0.1, 1.0]])

# Generate factor returns
factor_cov = np.diag(factor_vols) @ factor_corr @ np.diag(factor_vols)
factor_returns = np.random.multivariate_normal(factor_means, factor_cov, n_days)

# Asset betas (exposures to factors)
betas = np.random.uniform(-0.5, 1.5, (n_assets, 3))
# Add some structure: first 10 assets are equity-like (positive market beta)
betas[:10, 0] = np.random.uniform(0.8, 1.2, 10)
betas[10:15, 1] = np.random.uniform(0.5, 1.0, 5)  # rate-sensitive
betas[15:, 2] = np.random.uniform(0.6, 1.4, 5)    # FX-sensitive

# Idiosyncratic returns
idio_vol = np.random.uniform(0.005, 0.02, n_assets)
idio_returns = np.random.normal(0, idio_vol, (n_days, n_assets))

# Total asset returns: factor returns * betas + idiosyncratic
asset_returns = factor_returns @ betas.T + idio_returns

# Create DataFrame
asset_names = [f'Asset_{i+1}' for i in range(n_assets)]
returns_df = pd.DataFrame(asset_returns, columns=asset_names)
factor_df = pd.DataFrame(factor_returns, columns=['Market', 'Rates', 'FX'])

print("Portfolio Summary:")
print(f"  {n_assets} assets, {n_days} days")
print(f"  Factor correlation matrix:\n{factor_corr}")

# Portfolio weights (equal-weighted)
weights = np.ones(n_assets) / n_assets
port_returns = asset_returns @ weights

# ----------------------------------------------------------------
# PART B: FACTOR MODEL ESTIMATION
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART B: Factor Model Estimation (OLS)")
print("-"*60)

# Regress each asset return on the factors
from sklearn.linear_model import LinearRegression

betas_est = []
idios_est = []
r2_values = []

for i in range(n_assets):
    model = LinearRegression()
    model.fit(factor_returns, asset_returns[:, i])
    betas_est.append(model.coef_)
    idios_est.append(np.std(asset_returns[:, i] - model.predict(factor_returns)))
    r2_values.append(model.score(factor_returns, asset_returns[:, i]))

betas_est = np.array(betas_est)
idios_est = np.array(idios_est)

print("Estimated Betas (first 5 assets):")
betas_df = pd.DataFrame(betas_est[:5], columns=['Market', 'Rates', 'FX'])
betas_df.index = asset_names[:5]
print(betas_df.round(3))

print(f"\nAverage R²: {np.mean(r2_values):.4f}")
print(f"Average idiosyncratic volatility: {np.mean(idios_est):.4f}")

# Portfolio betas
port_betas = weights @ betas_est
print(f"\nPortfolio Betas: Market={port_betas[0]:.4f}, Rates={port_betas[1]:.4f}, FX={port_betas[2]:.4f}")

# ----------------------------------------------------------------
# PART C: PORTFOLIO RISK USING FACTOR COVARIANCE
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART C: Portfolio Risk – Factor Covariance Approach")
print("-"*60)

# Factor covariance matrix
factor_cov_est = np.cov(factor_returns.T)

# Idiosyncratic variance (diagonal)
idio_var = idios_est ** 2
idio_cov = np.diag(idio_var)

# Portfolio variance: beta_p^T * Sigma_F * beta_p + w^T * Sigma_idio * w
port_var_factor = port_betas @ factor_cov_est @ port_betas + weights @ idio_cov @ weights
port_vol_factor = np.sqrt(port_var_factor)

# Direct portfolio volatility (using historical returns)
port_vol_direct = np.std(port_returns, ddof=1)

print(f"Portfolio volatility (factor model): {port_vol_factor*100:.4f}%")
print(f"Portfolio volatility (direct): {port_vol_direct*100:.4f}%")
print(f"Difference: {(port_vol_factor - port_vol_direct)*100:.4f}%")

# VaR and ES using the factor model
confidence = 0.975  # FRTB uses 97.5% for ES
alpha = 1 - confidence
z_alpha = norm.ppf(alpha)

# 10-day VaR (scaling factor: sqrt(10))
scaling = np.sqrt(10)
var_10d_factor = -(0 + z_alpha * port_vol_factor * scaling)  # assuming zero mean
es_10d_factor = -(0 + port_vol_factor * scaling * norm.pdf(z_alpha) / alpha)

print(f"\n10-day VaR (97.5%): {var_10d_factor*100:.4f}%")
print(f"10-day ES (97.5%): {es_10d_factor*100:.4f}%")

# ----------------------------------------------------------------
# PART D: STRESS TESTING – HISTORICAL SCENARIOS
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART D: Stress Testing – Historical Scenarios")
print("-"*60)

# Define historical stress scenarios (as factor shocks)
stress_scenarios = {
    '2008 Financial Crisis': {'Market': -0.15, 'Rates': 0.02, 'FX': 0.05},
    'COVID-19 (Mar 2020)': {'Market': -0.12, 'Rates': -0.03, 'FX': 0.02},
    '2022 Inflation Shock': {'Market': -0.08, 'Rates': 0.04, 'FX': -0.03},
    'Tech Bubble (2000)': {'Market': -0.10, 'Rates': 0.01, 'FX': 0.01},
    'Extreme Hypothetical': {'Market': -0.25, 'Rates': 0.06, 'FX': 0.08}
}

# Function to compute portfolio impact
def portfolio_stress(factor_shocks, betas, weights):
    """Compute portfolio return under stress scenario."""
    asset_shocks = betas @ np.array([factor_shocks['Market'], 
                                     factor_shocks['Rates'], 
                                     factor_shocks['FX']])
    port_shock = weights @ asset_shocks
    return port_shock

stress_results = []
for name, shocks in stress_scenarios.items():
    port_loss = -portfolio_stress(shocks, betas_est, weights)  # positive loss
    stress_results.append({'Scenario': name, 'Portfolio Loss': port_loss})

stress_df = pd.DataFrame(stress_results)
stress_df['Portfolio Loss %'] = stress_df['Portfolio Loss'] * 100
print("\nStress Test Results (Portfolio Loss):")
print(stress_df.to_string(index=False))

# Visualise
fig, ax = plt.subplots(figsize=(10, 6))
colors = ['red' if loss > 0.10 else 'orange' if loss > 0.05 else 'blue' 
          for loss in stress_df['Portfolio Loss']]
ax.barh(stress_df['Scenario'], stress_df['Portfolio Loss %'], color=colors)
ax.set_xlabel('Portfolio Loss (%)')
ax.set_title('Stress Testing – Historical and Hypothetical Scenarios')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('market_stress_testing.png', dpi=300)
plt.show()

# ----------------------------------------------------------------
# PART E: HYPOTHETICAL SCENARIO GENERATION
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART E: Hypothetical Scenario Generation")
print("-"*60)

# Generate a range of hypothetical scenarios
def generate_hypothetical_scenarios(n_scenarios=10):
    """Generate random but plausible stress scenarios."""
    scenarios = []
    for i in range(n_scenarios):
        # Random shocks with correlations
        shocks = np.random.multivariate_normal(
            [-0.05, 0.01, 0.01],
            [[0.04, -0.01, 0.005],
             [-0.01, 0.01, -0.002],
             [0.005, -0.002, 0.02]]
        )
        # Clip to plausible ranges
        shocks = np.clip(shocks, [-0.20, -0.05, -0.10], [0.05, 0.08, 0.10])
        scenarios.append({
            'Market': shocks[0],
            'Rates': shocks[1],
            'FX': shocks[2]
        })
    return scenarios

hypothetical_scenarios = generate_hypothetical_scenarios(20)

# Evaluate portfolio loss under each scenario
hypo_losses = []
for shocks in hypothetical_scenarios:
    loss = -portfolio_stress(shocks, betas_est, weights)
    hypo_losses.append(loss)

hypo_losses = np.array(hypo_losses)

print(f"Hypothetical Scenario Losses:")
print(f"  Mean: {hypo_losses.mean()*100:.2f}%")
print(f"  Std: {hypo_losses.std()*100:.2f}%")
print(f"  Max: {hypo_losses.max()*100:.2f}%")
print(f"  Min: {hypo_losses.min()*100:.2f}%")

# 95th percentile of hypothetical losses
hypo_var_95 = np.percentile(hypo_losses, 95)
print(f"  95th percentile loss: {hypo_var_95*100:.2f}%")

# Visualise hypothetical losses
fig, ax = plt.subplots(figsize=(10, 5))
ax.hist(hypo_losses * 100, bins=15, edgecolor='black', alpha=0.7, color='purple')
ax.axvline(hypo_var_95 * 100, color='red', linestyle='--', 
           label=f'95th percentile: {hypo_var_95*100:.2f}%')
ax.set_xlabel('Portfolio Loss (%)')
ax.set_ylabel('Frequency')
ax.set_title('Hypothetical Scenario Loss Distribution')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('hypothetical_scenarios.png', dpi=300)
plt.show()

# ----------------------------------------------------------------
# PART F: PCA FOR RISK FACTOR REDUCTION
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART F: PCA – Risk Factor Reduction")
print("-"*60)

# Perform PCA on asset returns
pca = PCA()
pca.fit(asset_returns)

explained_var = pca.explained_variance_ratio_
cumulative_var = np.cumsum(explained_var)

print(f"First 3 principal components explain {cumulative_var[2]*100:.2f}% of variance")
print(f"Number of PCs for 95% variance: {np.argmax(cumulative_var >= 0.95) + 1}")

# Visualise
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

ax = axes[0]
ax.bar(range(1, len(explained_var)+1), explained_var, alpha=0.7, color='blue')
ax.set_xlabel('Principal Component')
ax.set_ylabel('Explained Variance Ratio')
ax.set_title('Variance Explained by Each PC')
ax.grid(True, alpha=0.3)

ax = axes[1]
ax.plot(range(1, len(cumulative_var)+1), cumulative_var, 'bo-', linewidth=2)
ax.axhline(y=0.95, color='red', linestyle='--', label='95% threshold')
ax.set_xlabel('Number of Components')
ax.set_ylabel('Cumulative Explained Variance')
ax.set_title('Cumulative Variance Explained')
ax.legend()
ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('pca_risk_factors.png', dpi=300)
plt.show()

# ----------------------------------------------------------------
# PART G: FRTB – STANDARDISED APPROACH (CONCEPTUAL)
# ----------------------------------------------------------------

print("\n" + "="*70)
print("PART G: FRTB – Standardised Approach (Conceptual)")
print("="*70)

print("""
FRTB Standardised Approach (SBM) – Key Components:

1. Risk Classes:
   - GIRR (General Interest Rate Risk)
   - CSR (Credit Spread Risk)
   - Equity Risk
   - FX Risk
   - Commodity Risk

2. Sensitivity-Based Method (SBM):
   - Calculate risk sensitivities (delta, vega, curvature) for each risk factor.
   - Apply risk weights (prescribed by the regulator) to each sensitivity.
   - Aggregate using correlation assumptions (within and across risk classes).

3. Default Risk Charge:
   - Additional capital for jump-to-default risk in debt instruments.

4. Simplified Implementation (Illustrative):
   - For a portfolio of equities, capital = 0.15 × portfolio value (simplified risk weight).
   - For a portfolio of bonds, capital = sensitivity × risk weight × volatility.
   
The actual calculation involves thousands of risk factors and is computationally intensive.
""")

# Simplified SA capital
sa_capital = 0.12 * 1000000  # 12% of portfolio value
print(f"Simplified SA Capital (illustrative): ${sa_capital:,.2f}")

# ----------------------------------------------------------------
# PART H: REGULATORY CAPITAL COMPARISON
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART H: Regulatory Capital Comparison")
print("-"*60)

# Portfolio value
portfolio_value = 1000000  # $1M

# IMA capital (based on ES)
es_10d = es_10d_factor * portfolio_value
ima_capital = es_10d  # Simplified: ES is the capital

# SA capital (simplified)
sa_capital_simplified = 0.12 * portfolio_value

print(f"Portfolio Value: ${portfolio_value:,.2f}")
print(f"\nFRTB Internal Models Approach (IMA) Capital: ${ima_capital:,.2f}")
print(f"FRTB Standardised Approach (SA) Capital: ${sa_capital_simplified:,.2f}")
print(f"Difference: SA is {(sa_capital_simplified/ima_capital - 1)*100:+.1f}% vs IMA")

# Backtesting requirement
print("""
Backtesting Requirements (FRTB):
  - Banks must backtest their internal models daily.
  - Test: compare 1-day VaR (99%) with actual P&L.
  - Green zone: 0-4 exceptions per year (acceptable).
  - Yellow zone: 5-9 exceptions (requires review).
  - Red zone: 10+ exceptions (model invalidated).
""")

SECTION 8: KEY REGULATORY REQUIREMENTS SUMMARY

 
 
Regulation Requirement Key Element
FRTB ES at 97.5% for IMA; SA with SBM. Replaces VaR with ES; desk-level approval.
Basel III Market risk capital; counterparty credit risk. SA-CCR; CVA risk capital.
SR 11-7 Model validation for market risk models. Independent validation; backtesting.
CCAR/DFAST Market risk stress testing. Scenario analysis with severe market shocks.

SECTION 9: SUMMARY FOR THE DATA PRACTITIONER

  • Market risk arises from movements in risk factors (interest rates, equities, FX, commodities, credit spreads).

  • FRTB is the new regulatory framework: ES (not VaR) for IMA; risk-sensitive SA using SBM.

  • Factor models decompose portfolio returns into systematic (factor) and idiosyncratic components.

  • Portfolio risk = factor covariance + idiosyncratic variance.

  • Stress testing uses historical and hypothetical scenarios to assess extreme losses.

  • PCA can reduce the dimensionality of risk factors.

  • In practice, banks use sophisticated systems with thousands of risk factors and Monte Carlo simulation.


SECTION 10: RECOMMENDED NEXT STEPS

  1. Apply factor models to a real portfolio (e.g., using Fama-French factors).

  2. Implement a full FRTB SA calculation for a simple portfolio.

  3. Learn about Expected Shortfall (ES) and its properties (sub-additivity).

  4. Explore Principal Component Analysis (PCA) for yield curve risk.

  5. Study the Internal Models Approach under FRTB in detail.

  6. Prepare for the next lesson on Asset-Liability Management (ALM) and Liquidity Risk.


[