03-信用衍生品 (Credit Derivatives)

预计学习时间:2-3 小时

难度:⭐⭐⭐⭐

核心问题:“借钱不还怎么办”这个最朴素的金融风险,怎么量化、怎么定价?


概述

信用衍生品处理的是金融世界中最古老的风险——违约风险

利率衍生品处理”利率变了怎么办”,信用衍生品处理”对方不还钱了怎么办”。后者的核心区别在于:违约是一个不可逆的跳跃事件,一旦发生就不可挽回。

"借钱不还怎么办" → 违约概率 → CDS → Merton 模型 → CDO → CVA/DVA

一、违约概率:借钱不还的可能性

1.1 违约概率的基本概念

白话解释:违约概率就是”这个借款人(或这家公司)在未来某个时间内不还钱的概率”。

概念含义例子
违约概率(PD)在给定时间内违约的概率未来 1 年违约概率 2%
违约损失率(LGD)违约时能收回多少违约后收回 40%,损失 60%
违约风险暴露(EAD)违约时暴露的金额贷款余额 1000 万
预期损失(EL)EL = PD × LGD × EAD2% × 60% × 1000 万 = 12 万

1.2 历史违约率 vs 预期违约概率

历史违约率:根据过去的违约数据统计出来的频率。比如”过去 20 年,BBB 评级公司中有 0.2% 在一年内违约”。

预期违约概率(风险中性):从市场价格(如债券收益率、CDS 利差)反推出来的违约概率。这个概率通常高于历史违约率,因为市场包含了风险溢价。

为什么两者不同?

  • 历史违约率是”平均情况”——但你买的债券可能运气不好
  • 市场价格还包含了违约风险补偿(风险溢价)
  • 市场还反映了投资者对未来经济环境的预期

1.3 从信用利差反推违约概率

信用利差 = 企业债券收益率 - 无风险债券收益率

白话解释:信用利差就是”投资者要求多拿多少利息,才愿意承担违约风险”。

如果我们做一个简化的假设(违约只发生在到期日,违约损失率 = LGD):

即:信用利差 ≈ 违约概率 × 违约损失率

更精确的连续复利近似:

import numpy as np
import matplotlib.pyplot as plt
 
# ============================================================
# 从信用利差反推违约概率
# ============================================================
 
def implied_pd(spread, lgd, T):
    """
    从信用利差反推风险中性违约概率
 
    参数:
        spread: 信用利差(年化,小数形式)
        lgd: 违约损失率(0-1)
        T: 期限(年)
    """
    # 简化公式: PD ≈ spread * T / LGD
    pd_simple = spread * T / lgd
 
    # 更精确的公式: PD = 1 - exp(-spread * T / LGD)
    pd_continuous = 1 - np.exp(-spread * T / lgd)
 
    return pd_simple, pd_continuous
 
# 不同信用评级的信用利差
ratings = ['AAA', 'AA', 'A', 'BBB', 'BB', 'B', 'CCC']
spreads = [0.005, 0.010, 0.020, 0.035, 0.060, 0.100, 0.200]  # 年化利差
T = 1.0       # 1 年期
lgd = 0.60    # 违约损失率 60%
 
print("从信用利差反推违约概率")
print("=" * 60)
print(f"{'评级':>6} {'信用利差':>10} {'违约概率(简化)':>15} {'违约概率(连续)':>15}")
print("-" * 55)
 
pds = []
for rating, spread in zip(ratings, spreads):
    pd_s, pd_c = implied_pd(spread, lgd, T)
    pds.append(pd_c)
    print(f"{rating:>6} {spread:>10.2%} {pd_s:>15.4%} {pd_c:>15.4%}")
 
# 可视化
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
 
# 左图:违约概率 vs 评级
colors = ['green', 'green', 'lime', 'yellow', 'orange', 'red', 'darkred']
axes[0].bar(range(len(ratings)), [p * 100 for p in pds], color=colors, alpha=0.7)
axes[0].set_xticks(range(len(ratings)))
axes[0].set_xticklabels(ratings)
axes[0].set_ylabel('违约概率(%)')
axes[0].set_title('各评级 1 年期违约概率(从信用利差反推)')
 
# 右图:违约概率 vs 信用利差
spread_range = np.linspace(0, 0.20, 100)
pd_range = [implied_pd(s, lgd, T)[1] for s in spread_range]
axes[1].plot(spread_range * 10000, np.array(pd_range) * 100, 'b-', lw=2)
axes[1].set_xlabel('信用利差(基点 bps)')
axes[1].set_ylabel('违约概率(%)')
axes[1].set_title(f'违约概率 vs 信用利差(LGD={lgd:.0%},T={T}年)')
axes[1].grid(True, alpha=0.3)
 
plt.tight_layout()
plt.show()
 
print(f"\n白话解释:")
print(f"  一个 BBB 评级(利差 350bps)的公司,1 年内违约概率约 {pds[3]:.2%}")
print(f"  一个 B 评级(利差 1000bps)的公司,1 年内违约概率约 {pds[5]:.2%}")
print(f"  评级越低 → 利差越高 → 违约概率越大 → 债券价格越低")

二、CDS(信用违约互换)

2.1 什么是 CDS

白话解释:CDS 就是”违约保险”。你每年付一笔保费给保险公司(保护卖方),如果公司违约了,保险公司赔你损失。

角色类比收益
保护买方(Buyer)买保险的人违约时获赔偿
保护卖方(Seller)卖保险的人收保费,违约时赔钱
参考实体(Reference Entity)被保险的标的违约则触发赔付
保费(Spread)保险费每年按名义本金的一定比例
名义本金(Notional)保险金额通常 1000 万美元

2.2 CDS 的运作方式

保护买方 ──每年付保费(Spread)──→ 保护卖方
保护买方 ←──违约时赔付─── 保护卖方

举例

你持有 A 公司发行的 1000 万美元债券。你担心 A 公司违约,于是买了一个 CDS:

  • 你每年付给保护卖方 200 个基点(2%),即每年 20 万
  • 如果 A 公司违约了,保护卖方赔给你 1000 万 × LGD

CDS 的两种赔付方式

  1. 实物交割:你把违约债券交给卖方,卖方付给你面值
  2. 现金结算:卖方直接付给你面值和债券残值的差额

2.3 CDS 定价(强度模型)

CDS 的定价基于违约强度(hazard rate)模型。

违约强度(:在极短时间 内违约的概率 ≈ 。你可以把它理解为”违约事件的瞬时发生率”,类似泊松过程。

在常数违约强度假设下:

  • 生存概率(到时间 没有违约):
  • 违约概率密度(在时间 附近违约):

CDS 定价的公式:

保费现值 =

赔付现值 =

其中 是 CDS 价差(保费率), 是回收率。

import numpy as np
import matplotlib.pyplot as plt
 
# ============================================================
# CDS 定价(简化强度模型)
# ============================================================
 
def cds_pricing(hazard_rate, recovery_rate, risk_free_rate, maturity, payment_freq=4):
    """
    CDS 定价(常数违约强度模型)
 
    参数:
        hazard_rate: 违约强度 λ(年化)
        recovery_rate: 回收率(违约后能收回的比例)
        risk_free_rate: 无风险利率
        maturity: CDS 期限(年)
        payment_freq: 每年付保费次数(默认季度)
    返回:
        cds_spread: 公平 CDS 价差(年化)
    """
    dt = 1.0 / payment_freq
    n_payments = int(maturity * payment_freq)
 
    # 保费端现值(Premium Leg)
    premium_pv = 0
    for i in range(1, n_payments + 1):
        t = i * dt
        survival_prob = np.exp(-hazard_rate * t)
        discount = np.exp(-risk_free_rate * t)
        premium_pv += discount * survival_prob * dt
 
    # 赔付端现值(Protection Leg)
    protection_pv = 0
    for i in range(1, n_payments + 1):
        t = i * dt
        # 在 [t-dt, t] 期间违约的概率
        default_prob = np.exp(-hazard_rate * (t - dt)) - np.exp(-hazard_rate * t)
        discount = np.exp(-risk_free_rate * t)
        protection_pv += (1 - recovery_rate) * default_prob * discount
 
    # 公平价差 = 赔付现值 / 保费现值因子
    cds_spread = protection_pv / premium_pv
 
    return cds_spread, premium_pv, protection_pv
 
# 参数
hazard_rate = 0.02       # 违约强度 2%(年化)
recovery_rate = 0.40     # 回收率 40%
risk_free_rate = 0.03    # 无风险利率 3%
maturity = 5.0           # 5 年期 CDS
 
spread, prem_pv, prot_pv = cds_pricing(hazard_rate, recovery_rate, risk_free_rate, maturity)
 
print("=" * 50)
print("CDS 定价(强度模型)")
print("=" * 50)
print(f"违约强度: {hazard_rate:.2%}")
print(f"回收率: {recovery_rate:.0%}")
print(f"无风险利率: {risk_free_rate:.2%}")
print(f"期限: {maturity} 年")
print(f"\n公平 CDS 价差: {spread:.4%} ({spread * 10000:.0f} bps)")
print(f"保费端现值系数: {prem_pv:.6f}")
print(f"赔付端现值: {prot_pv:.6f}(每 1 元名义本金)")
print()
print("白话解释:")
print(f"  为了保护 1 亿元的名义本金不违约,")
print(f"  你每年需要付保费 {spread*10000:.0f} bps,即 {spread*1e8:.0f} 元。")
print(f"  5 年总共付保费约 {spread*1e8*5:.0f} 元。")
 
# CDS 价差随违约强度的变化
lambda_range = np.linspace(0.005, 0.05, 50)
spreads = [cds_pricing(l, recovery_rate, risk_free_rate, maturity)[0]
           for l in lambda_range]
 
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
 
# 左图:价差 vs 违约强度
axes[0].plot(lambda_range * 100, np.array(spreads) * 10000, 'b-', lw=2)
axes[0].set_xlabel('违约强度 lambda(%)')
axes[0].set_ylabel('CDS 价差(bps)')
axes[0].set_title('CDS 价差 vs 违约强度')
axes[0].grid(True, alpha=0.3)
 
# 右图:不同期限的生存概率
t_range = np.linspace(0, 10, 200)
for lam, color in zip([0.01, 0.02, 0.03, 0.05], ['green', 'blue', 'orange', 'red']):
    survival = np.exp(-lam * t_range)
    axes[1].plot(t_range, survival * 100, color=color, lw=2,
                 label=f'lambda={lam:.0%}')
 
axes[1].set_xlabel('时间(年)')
axes[1].set_ylabel('生存概率(%)')
axes[1].set_title('不同违约强度下的生存概率')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
 
plt.tight_layout()
plt.show()
 
# 从市场 CDS 价差反推违约强度
def implied_hazard_rate(cds_spread, recovery_rate, risk_free_rate, maturity):
    """从 CDS 价差反推违约强度(二分法)"""
    low, high = 0.0001, 0.5
 
    for _ in range(100):
        mid = (low + high) / 2
        calculated_spread = cds_pricing(mid, recovery_rate, risk_free_rate, maturity)[0]
 
        if calculated_spread < cds_spread:
            low = mid
        else:
            high = mid
 
    return (low + high) / 2
 
market_cds = 0.03  # 市场价差 300 bps
implied_lambda = implied_hazard_rate(market_cds, recovery_rate, risk_free_rate, maturity)
print(f"\n市场 CDS 价差 = {market_cds*10000:.0f} bps")
print(f"反推违约强度 = {implied_lambda:.4%}")

三、信用评级与迁移矩阵

3.1 信用评级

评级机构(Moody’s、S&P、Fitch)给发债主体打分,用来量化其信用风险。

S&P 评级Moody’s 评级含义典型年违约率
AAAAaa最高信用质量~0.01%
AAAa很高信用质量~0.02%
AA高信用质量~0.06%
BBBBaa中等信用质量~0.18%
BBBa投机级~0.82%
BB高投机级~3.5%
CCC/CCaa/C极高投机级~15%+
DC违约100%

投资级 vs 投机级:BBB- 及以上是”投资级”(Investment Grade),BB+ 及以下是”投机级”或”垃圾债”(High Yield / Junk Bond)。这个分界线在实际金融中极其重要——很多机构投资者被法规限制只能买投资级债券。

import numpy as np
import matplotlib.pyplot as plt
 
# ============================================================
# 信用评级:违约率可视化
# ============================================================
 
ratings = ['AAA', 'AA', 'A', 'BBB', 'BB', 'B', 'CCC/C']
default_rates = [0.0001, 0.0002, 0.0006, 0.0018, 0.0082, 0.035, 0.15]
colors = ['darkgreen', 'green', 'limegreen', 'yellow', 'orange', 'red', 'darkred']
 
fig, ax = plt.subplots(figsize=(10, 5))
bars = ax.bar(range(len(ratings)), [d * 100 for d in default_rates],
              color=colors, alpha=0.8, edgecolor='black')
ax.set_xticks(range(len(ratings)))
ax.set_xticklabels(ratings, fontsize=12)
ax.set_ylabel('年违约率(%)')
ax.set_title('不同信用评级的典型年违约率')
ax.axvline(x=2.5, color='gray', linestyle='--', alpha=0.5)
ax.text(1, 0.12, '投资级', ha='center', fontsize=12, color='green')
ax.text(4.5, 0.12, '投机级', ha='center', fontsize=12, color='red')
ax.set_yscale('log')  # 对数坐标更好展示差异
ax.grid(True, alpha=0.3, axis='y')
 
plt.tight_layout()
plt.show()
 
print("关键观察:")
print("  从 AAA 到 BBB(投资级),违约率从 0.01% 到 0.18%,增加 18 倍")
print("  从 BB 到 CCC(投机级),违约率从 0.82% 到 15%,增加 18 倍")
print("  投资级和投机级之间的违约率差异跨越了两个数量级")

3.2 迁移矩阵(Transition Matrix)

公司的信用评级不是一成不变的。迁移矩阵描述的是:一年后,一个 BBB 评级的公司有多大可能变成 BB、A 或违约?

迁移矩阵示意(一年期,简化数据):

当前评级    AAA    AA     A    BBB    BB    B    CCC   违约
AAA       [0.91  0.08  0.01  0.00  0.00  0.00  0.00  0.00]
AA        [0.01  0.90  0.08  0.01  0.00  0.00  0.00  0.00]
A         [0.00  0.02  0.91  0.06  0.01  0.00  0.00  0.00]
BBB       [0.00  0.00  0.04  0.89  0.05  0.01  0.00  0.01]  ← 每行加起来 = 1
BB        [0.00  0.00  0.00  0.05  0.85  0.07  0.02  0.01]
B         [0.00  0.00  0.00  0.00  0.06  0.83  0.08  0.03]
CCC       [0.00  0.00  0.00  0.00  0.01  0.08  0.70  0.21]
D         [0.00  0.00  0.00  0.00  0.00  0.00  0.00  1.00]  ← 吸收态

白话解读(BBB 那一行):
  一个 BBB 公司,一年后:
  - 4% 的概率升到 A(信用改善)
  - 89% 的概率维持 BBB(不变)
  - 5% 的概率降到 BB(信用恶化)
  - 1% 的概率降到 B
  - 1% 的概率违约

多年累积迁移:迁移矩阵的幂次方。两年后的迁移概率 = ,五年后 =

import numpy as np
import matplotlib.pyplot as plt
 
# ============================================================
# 信用迁移矩阵模拟
# ============================================================
 
# 简化的 7 个评级 + 违约
rating_names = ['AAA', 'AA', 'A', 'BBB', 'BB', 'B', 'CCC', 'D']
 
# 一年期迁移矩阵(简化数据,非精确市场值)
M1 = np.array([
    # AAA   AA    A     BBB   BB    B    CCC   D
    [0.907, 0.083, 0.008, 0.001, 0.001, 0.000, 0.000, 0.000],  # AAA
    [0.007, 0.906, 0.078, 0.006, 0.002, 0.001, 0.000, 0.000],  # AA
    [0.001, 0.022, 0.910, 0.056, 0.008, 0.002, 0.001, 0.000],  # A
    [0.000, 0.003, 0.042, 0.892, 0.050, 0.009, 0.002, 0.002],  # BBB
    [0.000, 0.001, 0.004, 0.055, 0.847, 0.072, 0.015, 0.006],  # BB
    [0.000, 0.000, 0.002, 0.005, 0.068, 0.828, 0.075, 0.022],  # B
    [0.000, 0.000, 0.000, 0.008, 0.022, 0.105, 0.650, 0.215],  # CCC
    [0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 1.000],  # D(吸收态)
])
 
# 计算多年累积迁移概率
M5 = np.linalg.matrix_power(M1, 5)   # 5 年
M10 = np.linalg.matrix_power(M1, 10)  # 10 年
M20 = np.linalg.matrix_power(M1, 20)  # 20 年
 
# 对比不同期限的违约概率
print("=" * 65)
print("不同评级在不同时间跨度内的累积违约概率")
print("=" * 65)
print(f"{'评级':<8} {'1 年':>10} {'5 年':>10} {'10 年':>10} {'20 年':>10}")
print("-" * 50)
for i, rating in enumerate(rating_names[:-1]):  # 排除 D
    d1 = M1[i, -1]
    d5 = M5[i, -1]
    d10 = M10[i, -1]
    d20 = M20[i, -1]
    print(f"{rating:<8} {d1:>10.2%} {d5:>10.2%} {d10:>10.2%} {d20:>10.2%}")
 
# 可视化
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
 
# 左图:不同期限的违约概率对比
x = np.arange(len(rating_names[:-1]))
width = 0.2
d1 = [M1[i, -1] for i in range(len(rating_names) - 1)]
d5 = [M5[i, -1] for i in range(len(rating_names) - 1)]
d10 = [M10[i, -1] for i in range(len(rating_names) - 1)]
d20 = [M20[i, -1] for i in range(len(rating_names) - 1)]
 
axes[0].bar(x - 1.5*width, d1, width, label='1 年', color='steelblue')
axes[0].bar(x - 0.5*width, d5, width, label='5 年', color='orange')
axes[0].bar(x + 0.5*width, d10, width, label='10 年', color='coral')
axes[0].bar(x + 1.5*width, d20, width, label='20 年', color='darkred')
axes[0].set_xticks(x)
axes[0].set_xticklabels(rating_names[:-1])
axes[0].set_ylabel('累积违约概率')
axes[0].set_title('不同评级的累积违约概率随时间增长')
axes[0].legend()
axes[0].grid(True, alpha=0.3, axis='y')
 
# 右图:BBB 公司的评级分布随时间变化
bbb_idx = 3  # BBB 在数组中的索引
for years, color in [(1, 'steelblue'), (5, 'orange'), (10, 'coral'), (20, 'darkred')]:
    M_n = np.linalg.matrix_power(M1, years)
    dist = M_n[bbb_idx, :-1]  # 排除违约列
    axes[1].plot(range(len(rating_names) - 1), dist * 100, 'o-', color=color,
                 lw=2, label=f'{years} 年后')
 
axes[1].set_xticks(range(len(rating_names) - 1))
axes[1].set_xticklabels(rating_names[:-1])
axes[1].set_ylabel('概率(%)')
axes[1].set_title('BBB 公司的未来评级分布(随时间扩散)')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
 
plt.tight_layout()
plt.show()
 
print("\n白话解释:")
print("  一个 BBB 公司 1 年违约率只有 0.2%,看起来很安全")
print("  但 10 年违约率上升到 2% 以上,20 年超过 5%")
print("  这就是为什么长期债券的信用风险比短期债券大得多")
print("  评级分布随时间'扩散'——信用质量可能改善也可能恶化")

四、Merton 结构模型

3.1 核心思想:公司 = 期权

白话解释:Merton 模型最巧妙的地方在于——把一家公司看作一个期权。

想象一下:

  • 公司有资产(机器、专利、现金等),价值 ,随时在变动
  • 公司有负债(借款、债券等),面值 ,到期要还

在债券到期时:

  • 如果 :公司还清债务,股东拿到剩余部分 没违约
  • 如果 :公司资不抵债,违约,股东什么都没有 → 违约了

看!股东的权益 ——这不就是一个看涨期权吗?

标的资产 = 公司资产 ,行权价 = 负债面值 ,到期日 = 债券到期日。

3.2 用 BSM 框架推导信用利差

因为股权 是一个看涨期权,我们可以直接用 BSM 公式定价:

其中:

从风险中性角度,违约概率就是 (公司资产不足以覆盖负债的概率)。

信用利差近似为:

import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm
 
# ============================================================
# Merton 结构模型
# ============================================================
 
def merton_model(V, D, r, sigma_V, T, recovery_rate=0.4):
    """
    Merton 模型:将公司看作期权来评估违约风险
 
    参数:
        V: 公司资产价值
        D: 负债面值
        r: 无风险利率
        sigma_V: 资产波动率
        T: 负债到期时间
        recovery_rate: 回收率
    返回:
        dict: 股权价值、违约概率、信用利差等
    """
    d1 = (np.log(V / D) + (r + 0.5 * sigma_V**2) * T) / (sigma_V * np.sqrt(T))
    d2 = d1 - sigma_V * np.sqrt(T)
 
    # 股权价值(BSM Call)
    equity = V * norm.cdf(d1) - D * np.exp(-r * T) * norm.cdf(d2)
 
    # 违约概率(风险中性)
    pd_rn = norm.cdf(-d2)
 
    # 信用利差
    credit_spread = pd_rn * (1 - recovery_rate) / T
 
    # 距离违约的距离
    distance_to_default = d2
 
    return {
        'equity': equity,
        'pd_rn': pd_rn,
        'credit_spread': credit_spread,
        'd1': d1,
        'd2': d2,
        'distance_to_default': distance_to_default
    }
 
# 模拟两家公司
np.random.seed(42)
 
companies = {
    '稳健公司 A': {'V': 150, 'D': 100, 'sigma_V': 0.15},
    '高风险公司 B': {'V': 110, 'D': 100, 'sigma_V': 0.35},
}
 
r = 0.05
T = 1.0
 
print("=" * 60)
print("Merton 结构模型:两家公司的违约风险评估")
print("=" * 60)
print(f"{'':>15} {'资产V':>6} {'负债D':>6} {'D/V':>6} {'sigma_V':>8} "
      f"{'股权E':>8} {'违约概率':>10} {'信用利差':>10}")
print("-" * 80)
 
results = {}
for name, params in companies.items():
    res = merton_model(params['V'], params['D'], r, params['sigma_V'], T)
    results[name] = res
    print(f"{name:>15} {params['V']:>6} {params['D']:>6} "
          f"{params['D']/params['V']:>6.2f} {params['sigma_V']:>8.2%} "
          f"{res['equity']:>8.1f} {res['pd_rn']:>10.4%} "
          f"{res['credit_spread']:>10.2%}")
 
# 信用利差的 5 大决定因素
print("\n" + "=" * 50)
print("信用利差的 5 大决定因素(Merton 模型视角)")
print("=" * 50)
 
# 1. 资产/负债比
print("\n1. 杠杆率(D/V 越高,违约概率越大)")
V_range = np.linspace(80, 200, 50)
pd_vs_V = [merton_model(v, 100, r, 0.20, T)['pd_rn'] for v in V_range]
 
fig, axes = plt.subplots(2, 3, figsize=(16, 10))
 
axes[0, 0].plot(V_range, np.array(pd_vs_V) * 100, 'b-', lw=2)
axes[0, 0].axvline(x=100, color='red', linestyle='--', label='D=100')
axes[0, 0].set_xlabel('资产价值 V')
axes[0, 0].set_ylabel('违约概率(%)')
axes[0, 0].set_title('1. 杠杆率:资产价值越高越安全')
axes[0, 0].legend()
 
# 2. 资产波动率
print("2. 资产波动率(sigma_V 越大,违约概率越大)")
sigma_range = np.linspace(0.05, 0.50, 50)
pd_vs_sigma = [merton_model(120, 100, r, s, T)['pd_rn'] for s in sigma_range]
 
axes[0, 1].plot(sigma_range * 100, np.array(pd_vs_sigma) * 100, 'r-', lw=2)
axes[0, 1].set_xlabel('资产波动率 sigma_V(%)')
axes[0, 1].set_ylabel('违约概率(%)')
axes[0, 1].set_title('2. 波动率:资产波动越大越危险')
 
# 3. 到期时间
print("3. 到期时间(T 越长,违约概率越大——更长时间更多不确定性)")
T_range = np.linspace(0.1, 10, 50)
pd_vs_T = [merton_model(120, 100, r, 0.20, t)['pd_rn'] for t in T_range]
 
axes[0, 2].plot(T_range, np.array(pd_vs_T) * 100, 'g-', lw=2)
axes[0, 2].set_xlabel('到期时间 T(年)')
axes[0, 2].set_ylabel('违约概率(%)')
axes[0, 2].set_title('3. 期限:到期时间越长风险越大')
 
# 4. 回收率
print("4. 回收率(R 越低,信用利差越大——违约损失更大)")
R_range = np.linspace(0, 0.8, 50)
spread_vs_R = [merton_model(120, 100, r, 0.20, T, R)['credit_spread'] * 10000
               for R in R_range]
 
axes[1, 0].plot(R_range * 100, spread_vs_R, 'm-', lw=2)
axes[1, 0].set_xlabel('回收率 R(%)')
axes[1, 0].set_ylabel('信用利差(bps)')
axes[1, 0].set_title('4. 回收率:回收率越低利差越大')
 
# 5. 无风险利率
print("5. 无风险利率(影响较复杂,通常影响较小)")
r_range = np.linspace(0.01, 0.10, 50)
pd_vs_r = [merton_model(120, 100, ri, 0.20, T)['pd_rn'] for ri in r_range]
 
axes[1, 1].plot(r_range * 100, np.array(pd_vs_r) * 100, 'c-', lw=2)
axes[1, 1].set_xlabel('无风险利率(%)')
axes[1, 1].set_ylabel('违约概率(%)')
axes[1, 1].set_title('5. 利率:影响方向取决于具体情况')
 
# 综合热力图
V_grid = np.linspace(80, 180, 40)
sigma_grid = np.linspace(0.05, 0.50, 40)
VV, SS = np.meshgrid(V_grid, sigma_grid)
PP = np.array([[merton_model(v, 100, r, s, T)['pd_rn'] for v in V_grid] for s in sigma_grid])
im = axes[1, 2].contourf(VV, SS, PP * 100, levels=20, cmap='RdYlGn_r')
plt.colorbar(im, ax=axes[1, 2])
axes[1, 2].set_xlabel('资产价值 V')
axes[1, 2].set_ylabel('资产波动率 sigma_V')
axes[1, 2].set_title('违约概率热力图(资产 vs 波动率)')
 
plt.tight_layout()
plt.show()
 
print("\n五大因素总结:")
print("  杠杆高(D/V大)→ 危险")
print("  波动大(sigma大)→ 危险")
print("  期限长(T大)→ 危险")
print("  回收低(R小)→ 损失大")
print("  利率影响复杂 → 需具体分析")

3.3 Merton 模型的局限

局限具体问题
只有一个到期日真实公司有多种到期日的债券
违约只在到期时真实违约可能在任何时候发生
资产价值不可观测只能从股价和波动率反推
假设资产服从 GBM真实资产分布有跳跃和厚尾
忽略债务优先级不同债券的优先级不同

五、CDO/CLO 结构化产品

4.1 什么是 CDO

白话解释:CDO(Collateralized Debt Obligation)是一种”打包再分拆”的产品。

想象你有一筐苹果:

  • 有些苹果品质好(AAA 评级债券)
  • 有些苹果品质一般(BBB 评级债券)
  • 有些苹果有点烂(BB 评级债券)

CDO 的做法是:把这筐苹果打成汁,然后分成几杯果汁:

层级(Tranche)特征类比
高级层(Senior)最先受保护,违约时最后受损”好果汁”——用最好的苹果做
夹层(Mezzanine)中等保护”普通果汁”——混在一起
股权层(Equity)最先吸收损失,收益率最高”剩渣”——但便宜,高风险高回报

白话解读:你买了高级层,就算底层有少量债券违约,也不会影响你——因为股权层先扛损失。只有当违约严重到把股权层和夹层都”烧光”了,高级层才会受损。

4.2 CLO(贷款抵押债券)

CLO 和 CDO 类似,但底层资产是银行贷款而不是债券。CLO 是银行管理资本的一种方式——把贷款打包出售,释放资本金。

4.3 2008 年金融危机与 CDO

2008 年金融危机的根源之一就是 CDO:

  1. 底层资产质量差:次级房贷(subprime mortgage)被包装成 AAA 的 CDO
  2. 评级失灵:评级机构给了高风险资产 AAA 评级
  3. 相关性假设错误:模型假设不同地区的房贷违约是不相关的(分散化),但房地产泡沫破裂时大家同时违约
  4. 杠杆叠加:CDO 上面还有 CDO²(CDO 的 CDO),杠杆率极高

关键教训:模型假设的”分散化”在系统性危机中完全失效。

import numpy as np
import matplotlib.pyplot as plt
 
# ============================================================
# CDO 结构化产品模拟
# ============================================================
 
def simulate_cdo(n_bonds, pd_per_bond, lgd, n_sims=100000, seed=42):
    """
    模拟 CDO 的损失分布
 
    参数:
        n_bonds: 底层债券数量
        pd_per_bond: 每个债券的违约概率
        lgd: 违约损失率
        n_sims: 模拟次数
    """
    np.random.seed(seed)
 
    # 模拟违约(假设独立)
    defaults = np.random.binomial(1, pd_per_bond, size=(n_sims, n_bonds))
    total_losses = np.sum(defaults, axis=1) * lgd  # 总损失(占名义本金的比例)
 
    return total_losses
 
# 参数
n_bonds = 100         # 100 个底层债券
pd_per_bond = 0.05    # 每个债券 5% 违约概率
lgd = 0.60            # 违约损失率 60%
notional = 1000       # 总名义本金 1000
 
# 模拟损失分布
losses_pct = simulate_cdo(n_bonds, pd_per_bond, lgd / n_bonds)
 
# 定义分层
tranches = {
    'Equity (0-3%)':   {'attachment': 0.00, 'detachment': 0.03},
    'Mezzanine (3-7%)': {'attachment': 0.03, 'detachment': 0.07},
    'Senior (7-12%)':   {'attachment': 0.07, 'detachment': 0.12},
    'Super Senior (12%+)': {'attachment': 0.12, 'detachment': 1.00},
}
 
print("=" * 60)
print("CDO 结构化产品模拟")
print("=" * 60)
print(f"底层债券数量: {n_bonds}")
print(f"每个债券违约概率: {pd_per_bond:.2%}")
print(f"违约损失率: {lgd:.0%}")
print(f"总名义本金: {notional}")
print()
 
# 计算每层的损失
for name, params in tranches.items():
    a, d = params['attachment'], params['detachment']
    # 该层的损失 = min(总损失, d) - min(总损失, a)
    layer_loss = np.minimum(losses_pct, d) - np.minimum(losses_pct, a)
    expected_loss = np.mean(layer_loss)
    max_loss = np.max(layer_loss)
 
    print(f"{name:>25} [{a*100:.0f}%-{d*100:.0f}%]:")
    print(f"  预期损失: {expected_loss:.4%} of notional "
          f"= {expected_loss * notional:.2f}")
    print(f"  最大损失: {max_loss:.4%} of notional")
    print()
 
# 可视化
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
 
# 左图:损失分布
axes[0].hist(losses_pct * 100, bins=100, density=True, alpha=0.7, color='steelblue')
axes[0].axvline(x=3, color='red', linestyle='--', label='Equity/Mezz 边界 (3%)')
axes[0].axvline(x=7, color='orange', linestyle='--', label='Mezz/Senior 边界 (7%)')
axes[0].axvline(x=12, color='green', linestyle='--', label='Senior/Super 边界 (12%)')
axes[0].set_xlabel('总损失率(%)')
axes[0].set_ylabel('概率密度')
axes[0].set_title('CDO 底层资产损失分布')
axes[0].legend()
 
# 右图:分层结构图
y_positions = [4, 3, 2, 1]
widths = [(3-0), (7-3), (12-7), (100-12)]
colors = ['darkred', 'orange', 'steelblue', 'green']
labels = ['Equity\n(0-3%)', 'Mezzanine\n(3-7%)', 'Senior\n(7-12%)', 'Super Senior\n(12%-100%)']
 
for y, w, c, l in zip(y_positions, widths, colors, labels):
    axes[1].barh(y, w, left=[0, 3, 7, 12][y_positions.index(y)],
                 color=c, alpha=0.7, height=0.6, edgecolor='black')
    x_center = [0, 3, 7, 12][y_positions.index(y)] + w / 2
    axes[1].text(x_center, y, l, ha='center', va='center', fontsize=9, fontweight='bold')
 
axes[1].set_xlabel('损失吸收(%)')
axes[1].set_title('CDO 分层结构')
axes[1].set_yticks([])
axes[1].set_xlim(0, 30)
 
plt.tight_layout()
plt.show()
 
print("白话解释:")
print("  Equity 层最先吸收损失——损失不超过 3% 时,只有 Equity 层受损")
print("  Mezzanine 层在 3%-7% 之间吸收损失")
print("  Senior 层在 7%-12% 之间")
print("  Super Senior 只有在损失超过 12% 时才会受损")
print()
print("  2008 年的问题:底层房贷质量太差,损失轻松超过 12%,")
print("  'AAA 评级'的 Super Senior 层也大规模亏损。")

4.4 违约相关性与 Copula 模型

上面的 CDO 模拟假设各债券的违约是独立的。但在现实中,经济恶化时企业会同时违约——这就是违约相关性

白话解释

  • 独立违约:100 家公司,每家 2% 违约率,平均 2 家违约,波动很小
  • 高度相关:如果经济危机来了,可能 20 家同时违约;如果经济好,0 家违约

相关性悖论(这是 CDO 定价中最重要的反直觉结论):

违约相关性对 CDO 各层的影响:

相关性上升时:
  权益层(Equity)  → 风险先升后降
  夹层(Mezzanine) → 风险显著上升
  高级层(Senior)  → 风险大幅上升(分散化失效!)

直觉解释:
  低相关性 → 违约分散,几乎肯定有少量违约 → 权益层持续受损
  高相关性 → 要么一起违约(大损失),要么都没事 → 高级层开始受损
import numpy as np
from scipy.stats import norm
import matplotlib.pyplot as plt
 
# ============================================================
# CDO 定价:高斯 Copula 模型(违约相关性)
# ============================================================
 
def simulate_cdo_copula(n_names, n_sim, pd, lgd, correlation,
                        tranche_attachments):
    """
    高斯 Copula 模型模拟 CDO 各层损失分布
 
    使用单因子模型:
      X_i = sqrt(rho) * Z + sqrt(1-rho) * eps_i
      违约条件: X_i < N^(-1)(PD)
 
    参数:
        n_names: 底层债务人数量
        n_sim: 模拟次数
        pd: 每个债务人的违约概率
        lgd: 违约损失率
        correlation: 违约相关性 rho
        tranche_attachments: {层名: (下界, 上界)}
    返回:
        tranche_losses: 各层的损失率
        portfolio_losses: 资产池总损失率
    """
    threshold = norm.ppf(pd)  # 违约阈值
 
    portfolio_losses = np.zeros(n_sim)
 
    for sim in range(n_sim):
        # 系统性因子(所有债务人共享)
        Z = np.random.randn()
 
        # 条件违约概率
        conditional_pd = norm.cdf(
            (threshold - np.sqrt(correlation) * Z)
            / np.sqrt(1 - correlation)
        )
 
        # 模拟违约
        defaults = np.random.rand(n_names) < conditional_pd
        loss = np.mean(defaults) * lgd
        portfolio_losses[sim] = loss
 
    # 各层损失
    tranche_losses = {}
    for name, (lower, upper) in tranche_attachments.items():
        layer_loss = np.minimum(
            np.maximum(portfolio_losses - lower, 0), upper - lower
        )
        tranche_losses[name] = layer_loss / (upper - lower)
 
    return tranche_losses, portfolio_losses
 
# 参数
n_names = 100
n_sim = 50000
pd = 0.02
lgd = 0.60
 
tranches = {
    'Equity':  (0.00, 0.03),
    'Mezzanine': (0.03, 0.07),
    'Senior':  (0.07, 0.12),
    'Super Senior': (0.12, 1.00),
}
 
np.random.seed(42)
 
# 不同相关性下各层的预期损失
corr_range = np.linspace(0.01, 0.80, 20)
results = {name: [] for name in tranches}
 
for corr in corr_range:
    tl, _ = simulate_cdo_copula(
        n_names, n_sim, pd, lgd, corr, tranches
    )
    for name in tranches:
        results[name].append(np.mean(tl[name]))
 
# 可视化
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
 
# 左图:各层预期损失 vs 相关性
colors_map = {'Equity': 'darkred', 'Mezzanine': 'orange',
              'Senior': 'steelblue', 'Super Senior': 'green'}
for name in tranches:
    axes[0].plot(corr_range * 100,
                 np.array(results[name]) * 100,
                 lw=2, label=name, color=colors_map[name])
 
axes[0].set_xlabel('违约相关性 rho(%)')
axes[0].set_ylabel('层内预期损失率(%)')
axes[0].set_title('违约相关性对 CDO 各层预期损失的影响')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
 
# 右图:不同相关性下资产池损失分布
for corr_val, color, label in [(0.05, 'green', 'rho=5%'),
                                (0.30, 'blue', 'rho=30%'),
                                (0.70, 'red', 'rho=70%')]:
    np.random.seed(42)
    _, pl = simulate_cdo_copula(
        n_names, n_sim, pd, lgd, corr_val, tranches
    )
    axes[1].hist(pl * 100, bins=80, density=True, alpha=0.5,
                 color=color, label=label)
 
axes[1].set_xlabel('资产池总损失率(%)')
axes[1].set_ylabel('概率密度')
axes[1].set_title('不同违约相关性下的资产池损失分布')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
 
plt.tight_layout()
plt.show()
 
print("关键观察:")
print("  1. Equity 层在低相关性时损失较高(总是有少量违约)")
print("  2. Senior 层在高相关性时损失急剧上升(分散化失效)")
print("  3. 高相关性下损失分布变得双峰(要么没事,要么一起爆)")
print()
print("2008 年危机的本质:")
print("  相关性从 5% 飙升到 70%+ → 分散化完全失效")
print("  → 原本'安全'的 Senior 和 Super Senior 层大规模亏损")

六、Counterparty 风险:CVA / DVA

5.1 对手方风险

白话解释:你和银行签了一个衍生品合约,银行欠你钱。但如果银行倒闭了呢?你的合约价值就变成了废纸。

这就是对手方风险(Counterparty Risk):交易对手违约,导致你遭受损失的风险。

在 2008 年之前,很多人认为这种风险可以忽略(大型银行”大而不倒”)。2008 年之后,大家意识到必须量化这种风险。

5.2 CVA(Credit Valuation Adjustment)

CVA = 无对手方风险的衍生品价值 - 有对手方风险的衍生品价值

白话解释:CVA 是因为对手方可能违约而导致的”价值折扣”。你需要把衍生品的价值打一个折扣来反映对手方风险。

CVA 的简化公式:

其中:

变量含义
违约损失率
预期正暴露(Expected Positive Exposure)——你的衍生品在 时刻暴露给对手方的价值
时刻的违约概率密度

白话解读

  • EPE:如果对手方在 时刻违约,你大概会损失多少?(只有衍生品价值为正时才暴露风险)
  • CVA 就是把每个时刻的”可能损失”按违约概率加权求和

5.3 DVA(Debt Valuation Adjustment)

白话解释:CVA 是”别人可能不还我钱”的风险折扣。DVA 是”我可能不还别人钱”的风险调整。

当你的信用状况恶化时,DVA 上升——你欠别人的钱”变便宜了”。这在直觉上很奇怪:你自己信用越差,你的衍生品价值反而越高(因为 DVA 增加了)。

这就是”mark-to-market”会计的一个反直觉特性。2008 年金融危机时,很多银行的财报中出现了 DVA 带来的”利润”——因为它们自己的信用评级被下调了。

5.4 Python 模拟 CVA

import numpy as np
import matplotlib.pyplot as plt
 
# ============================================================
# CVA 计算(简化版蒙特卡洛)
# ============================================================
 
def calculate_cva(notional, r, hazard_rate, lgd, n_paths=50000, n_steps=50):
    """
    简化版 CVA 计算
 
    参数:
        notional: 衍生品名义本金
        r: 无风险利率
        hazard_rate: 对手方违约强度
        lgd: 违约损失率
        n_paths: 蒙特卡洛路径数
        n_steps: 时间步数
    """
    T = 5.0  # 衍生品期限 5 年
    dt = T / n_steps
    times = np.linspace(dt, T, n_steps)
 
    # 1. 模拟衍生品价值路径(简化:假设是一个利率互换的固定端价值)
    # 互换价值 ≈ 收到的浮动利率现值 - 付出的固定利率现值
    # 这里用一个简单的随机过程模拟
    np.random.seed(42)
    W = np.random.standard_normal((n_steps, n_paths))
 
    # 简化的互换价值模型
    swap_value = np.zeros((n_steps, n_paths))
    sigma_swap = 0.02  # 互换价值波动率
    for i in range(n_steps):
        drift = 0.001  # 轻微的正漂移(假设收到浮动利率略高于固定利率)
        swap_value[i] = swap_value[i-1] if i > 0 else 0.01 * notional
        swap_value[i] += swap_value[i] * drift * dt
        swap_value[i] += swap_value[i] * sigma_swap * np.sqrt(dt) * W[i]
        swap_value[i] = np.maximum(swap_value[i], -0.3 * notional)  # 下限
 
    # 2. 计算每个时间步的 EPE(Expected Positive Exposure)
    epe = np.array([np.mean(np.maximum(swap_value[i], 0)) for i in range(n_steps)])
 
    # 3. 计算每个时间步的违约概率
    default_probs = np.array([
        np.exp(-hazard_rate * times[i-1]) - np.exp(-hazard_rate * times[i])
        if i > 0 else 1 - np.exp(-hazard_rate * dt)
        for i in range(n_steps)
    ])
 
    # 4. 计算 CVA
    discount_factors = np.exp(-r * times)
    cva = -lgd * np.sum(epe * default_probs * discount_factors) * dt
 
    return cva, epe, times
 
# 参数
notional = 100_000_000  # 1 亿
r = 0.03
hazard_rate = 0.02       # 对手方违约强度 2%
lgd = 0.60
 
cva, epe, times = calculate_cva(notional, r, hazard_rate, lgd)
 
print("=" * 50)
print("CVA(信用估值调整)计算")
print("=" * 50)
print(f"名义本金: {notional/1e8:.0f} 亿")
print(f"对手方违约强度: {hazard_rate:.2%}")
print(f"违约损失率: {lgd:.0%}")
print(f"无风险利率: {r:.2%}")
print(f"\nCVA = {cva:,.0f} 元")
print(f"CVA 占名义本金比例 = {abs(cva)/notional:.4%}")
print()
print("白话解释:")
print(f"  因为对手方可能违约,你的衍生品合约需要打折")
print(f"  CVA = {abs(cva)/1e4:.0f} 万元")
print(f"  这就是对手方风险的'价格'")
 
# 可视化
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
 
# 左图:EPE 随时间变化
axes[0].plot(times, epe / 1e6, 'b-', lw=2)
axes[0].fill_between(times, 0, epe / 1e6, alpha=0.2, color='blue')
axes[0].set_xlabel('时间(年)')
axes[0].set_ylabel('EPE(百万元)')
axes[0].set_title('预期正暴露(EPE)随时间变化')
axes[0].grid(True, alpha=0.3)
 
# 右图:CVA 组成
survival = np.exp(-hazard_rate * times)
default_density = hazard_rate * np.exp(-hazard_rate * times)
 
ax2 = axes[1]
ax2.plot(times, survival * 100, 'g-', lw=2, label='生存概率')
ax2_twin = ax2.twinx()
ax2_twin.plot(times, default_density * 100, 'r--', lw=1.5, label='违约密度')
ax2.set_xlabel('时间(年)')
ax2.set_ylabel('生存概率(%)', color='green')
ax2_twin.set_ylabel('违约密度(%)', color='red')
ax2.set_title('生存概率与违约密度')
 
# 合并图例
lines1, labels1 = ax2.get_legend_handles_labels()
lines2, labels2 = ax2_twin.get_legend_handles_labels()
ax2.legend(lines1 + lines2, labels1 + labels2, loc='center right')
 
plt.tight_layout()
plt.show()
 
# 不同对手方信用质量的 CVA 对比
print("\n不同对手方信用质量下的 CVA 对比")
print("-" * 50)
print(f"{'违约强度':>10} {'CVA(万元)':>12} {'占名义比例':>12}")
print("-" * 40)
 
for lam in [0.005, 0.01, 0.02, 0.05, 0.10]:
    cva_val = calculate_cva(notional, r, lam, lgd)[0]
    print(f"{lam:>10.2%} {abs(cva_val)/1e4:>12.2f} {abs(cva_val)/notional:>12.4%}")

七、信用衍生品全景

6.1 从简单到复杂的信用风险层次

违约概率(PD) ← 最基础的度量
    ↓
CDS(违约保险) ← 单个实体的违约保护
    ↓
Merton 模型 ← 从公司基本面推导违约概率
    ↓
CDO/CLO ← 一篮子实体的分层违约保护
    ↓
CVA/DVA ← 衍生品交易中的对手方信用风险调整
    ↓
FVA/KVA/XVA ← 更全面的风险调整框架

6.2 信用风险 vs 市场风险

维度市场风险信用风险
来源价格波动(利率、股价、汇率)交易对手违约
频率每天都在波动违约是稀有事件
分布近似正态(但厚尾)严重偏斜(大部分时间 0,违约时巨大损失)
对冲容易(Delta、Gamma 对冲)困难(需要 CDS 或担保品)
度量VaR、GreeksPD、LGD、EAD、CVA

八、关键概念回顾

概念一句话解释
违约概率(PD)未来某时间内不还钱的概率
违约损失率(LGD)违约时损失的本金比例
信用利差企业债收益率 - 国债收益率 = 违约风险补偿
信用评级AAA ~ D 的分级体系;BBB- 是投资级分界线
迁移矩阵描述评级随时间变化的概率;多年迁移 = 矩阵幂次
CDS违约保险,每年付保费,违约时获赔付
违约强度(瞬时违约发生率,类似泊松过程参数
Merton 模型把公司看作期权,从资产/负债结构推导违约概率
违约距离(DD)资产价值偏离违约临界点的标准差个数
CDO打包再分拆——底层资产损失按层级吸收
高斯 Copula用相关因子模型描述多资产联合违约
相关性悖论相关性上升对高级层更危险(分散化失效)
CVA因为对手方可能违约而给衍生品打的折扣
DVA因为”我”可能违约而给衍生品做的调整

推荐阅读

书名章节核心价值
Options, Futures, and Other Derivatives (Hull)Ch.24-25CDS、CDO、CVA 的权威参考
Credit Risk Modeling (Lando)Ch.2-5违约强度模型的严格推导
The XVA Challenge (Gregory)全书CVA/DVA/FVA 的实务指南

版本信息

  • 创建日期:2026-03-28
  • 最后更新:2026-03-28