34  金融模型分析 相关性分析

34.1 引言相关性在金融中的核心地位

相关性的金融意义:

相关性分析是现代金融学的基石。从资本资产定价模型(CAPM)到风险价值(VaR)计算,从投资组合优化到对冲策略设计,相关性无处不在。

理论背景:相关性的数学定义

皮尔逊相关系数(Pearson Correlation Coefficient)衡量两个变量线性相关的强度和方向:

\[ \rho_{X,Y} = \frac{\text{Cov}(X,Y)}{\sigma_X \sigma_Y} = \frac{E[(X-\mu_X)(Y-\mu_Y)]}{\sigma_X \sigma_Y} \]

其中: - \(\text{Cov}(X,Y)\): 协方差 - \(\sigma_X, \sigma_Y\): 标准差 - 取值范围: \([-1, 1]\)

金融应用中的相关性:

  1. 投资组合分散化:
    • 投资组合方差: \(\sigma_p^2 = \sum_i \sum_j w_i w_j \sigma_i \sigma_j \rho_{ij}\)
    • 相关性越低,组合风险越小
    • 马科维茨理论的核心
  2. 系统性风险识别:
    • 高相关性意味着暴露于共同因子
    • 市场崩盘时可能同跌
    • 分散化失效的风险
  3. 风险对冲策略:
    • 负相关资产可用于对冲
    • 寻找反向变动资产
    • 构建市场中性策略
  4. 因子模型构建:
    • 相关性聚类识别行业因子
    • 风格因子暴露分析
    • 多因子模型验证

34.2 获取指数数据

34.2.1 数据源与API

补充说明:A股指数体系

中国A股市场主要指数:

  1. 上证综指(000001.SH):
    • 包含上海证券交易所所有股票
    • 以总市值为权重
    • 反映沪市整体表现
  2. 深证成指(399001.SZ):
    • 包含深圳证券交易所500只股票
    • 综合考虑规模、流动性
    • 反映深市整体表现
  3. 创业板指(399006.SZ):
    • 创业板市场的代表性指数
    • 高成长性、高风险
    • 科技股集中
列表 34.1
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
# 注:Tushare接口权限不足,如要运行,可能需要更换api key
import numpy as np  # 导入NumPy数值计算库
import pandas as pd  # 导入Pandas数据分析库
import seaborn as sns  # 导入Seaborn可视化库
import matplotlib.pyplot as plt  # 导入Matplotlib绑图库
import tushare as ts  # 导入tushare模块

# 初始化Tushare api
pro = ts.pro_api("ba1646815a79a63470552889a69f957f5544bef01d3f082159bf8474")

# 获取上证指数、深证指数和创业板三个指数的收盘价
sh = pro.index_daily(ts_code="000001.SH", start_date="20220101", end_date="20230418")
sz = pro.index_daily(ts_code="399001.SZ", start_date="20220101", end_date="20230418")  # 获取深证成指日线行情数据
cyb = pro.index_daily(ts_code="399006.SZ", start_date="20220101", end_date="20230418")  # 获取创业板指日线行情数据

# 将数据框中的交易日期格式化为日期,并将其设置为索引
sh["trade_date"] = pd.to_datetime(sh["trade_date"], format="%Y%m%d")
sh.set_index("trade_date", inplace=True)  # 将trade_date列设为sh数据框的索引

sz["trade_date"] = pd.to_datetime(sz["trade_date"], format="%Y%m%d")  # 转换为日期时间格式
sz.set_index("trade_date", inplace=True)  # 将trade_date列设为sz数据框的索引

cyb["trade_date"] = pd.to_datetime(cyb["trade_date"], format="%Y%m%d")  # 转换为日期时间格式
cyb.set_index("trade_date", inplace=True)  # 将trade_date列设为cyb数据框的索引

# 合并收盘价
data = pd.concat([sh["close"], sz["close"], cyb["close"]], axis=1)
data.columns = ["SH", "SZ", "CYB"]  # 定义列表data.columns

# 计算收益率
returns = np.log(data / data.shift(1))

# 绘制相关矩阵热力图
sns.set(style="white")
corr = returns.corr().round(2)  # 计算相关系数矩阵
mask = np.zeros_like(corr, dtype=bool)  # 创建布尔掩码矩阵(用于隐藏热力图上三角)
# mask[np.triu_indices_from(mask)] = True
f, ax = plt.subplots(figsize=(9, 6))
cmap = sns.diverging_palette(220, 10, as_cmap=True)  # 创建蓝-红发散色板用于相关性热力图
# 绑制热力图
sns.heatmap(corr, annot=True, cmap=cmap, vmax=1, vmin=-1, center=0, square=True, linewidths=.5, cbar_kws={"shrink": .5})
ax.set_title("Correlation Matrix")  # 设置图表标题
plt.savefig("1.png")  # 保存图形至文件

代码深度解析:

  1. API调用:
    • tushare: A股免费数据源
    • pro_api(): 初始化pro接口
    • index_daily(): 获取日线数据
  2. 日期处理:
    • format="%Y%m%d": Tushare日期格式
    • set_index(): 将日期设为索引
    • 便于后续时间序列操作
  3. 数据合并:
    • pd.concat(): 横向合并DataFrame
    • axis=1: 按列合并
    • 自动对齐日期索引

34.3 计算收益率与相关性

34.3.1 收益率计算

补充说明:简单收益率 vs 对数收益率

特性 简单收益率 对数收益率
公式 \(R_t = (P_t - P_{t-1}) / P_{t-1}\) \(r_t = \ln(P_t / P_{t-1})\)
可加性 多期收益率不可直接相加 可相加: \(r_{1→3} = r_1 + r_2 + r_3\)
对称性 不对称: +10%和-10%不对等 对称
分布 偏态 更接近正态分布
金融应用 报告收益 计算波动率、建模

相关性分析建议: 使用对数收益率,因为: 1. 更接近正态分布 2. 时间可加性 3. 适合连续复利假设

列表 34.2
# 注:该代码块依赖的数据来自上方平台任务代码块,因其未执行,本块也无法执行

# ==================== 计算对数收益率 ====================
# 对数收益率的计算公式: r_t = ln(P_t / P_{t-1})
# 相当于: r_t = ln(P_t) - ln(P_{t-1})
returns = np.log(data / data.shift(1))
# data.shift(1): 将数据向下移动一行,使P_t与P_{t-1}对齐
# np.log(): 计算自然对数

# ==================== 删除第一行 ====================
# 第一行没有上期数据,计算结果为NaN,需要删除
returns = returns.dropna()  # 删除包含NaN的行

# ==================== 收益率数据探索 ====================
print('收益率数据:')
print(returns.head())  # 显示前5行收益率数据

# ==================== 收益率统计 ====================
print(f'\n收益率描述统计:')
print(returns.describe().T)  # .T转置,使指数为行,统计量为列
# 输出解读:观察各指数的收益率均值(平均收益)和标准差(波动率)

# ==================== 计算相关系数矩阵 ====================
# 使用皮尔逊相关系数计算指数间的相关性
corr_matrix = returns.corr()  # 计算相关系数矩阵
# corr()默认使用pearson相关系数

print('\n相关系数矩阵:')
print(corr_matrix.round(4))  # 保留4位小数
# 输出解读:
# 对角线为1(自己与自己的相关系数)
# 非对角线元素表示不同指数间的相关性
# 值越接近1,正相关越强;越接近-1,负相关越强

# ==================== 验证对称性 ====================
print(f'\n矩阵对称性: {np.allclose(corr_matrix, corr_matrix.T)}')
# 相关系数矩阵应该是对称的(ρ_ij = ρ_ji)
print(f'对角线元素: {np.diag(corr_matrix)}')
# 对角线元素应该全为1(ρ_ii = 1)

代码深度解析:

  1. 对数收益率计算:
    • np.log(): 自然对数
    • data.shift(1): 向下移动一行
    • P_t / P_{t-1}: 价格比
  2. 相关系数矩阵性质:
    • 对称性: \(\rho_{ij} = \rho_{ji}\)
    • 对角线为1: \(\rho_{ii} = 1\)
    • 正定性: 协方差矩阵半正定
  3. .corr()方法参数:
    • method='pearson': 皮尔逊相关(默认)
    • method='spearman': 斯皮尔曼秩相关
    • method='kendall': 肯德尔τ相关

34.4 可视化相关性

34.4.1 基础热力图

列表 34.3
# 注:该代码块依赖的数据来自上方平台任务代码块,因其未执行,本块也无法执行

# ==================== 设置绘图风格 ====================
sns.set(style="white")  # 使用白色背景风格
f, ax = plt.subplots(figsize=(9, 6))  # 创建9x6英寸的画布

# ==================== 创建发散调色板 ====================
# 发散调色板适合有正负值的数据,0为中性色
cmap = sns.diverging_palette(220, 10, as_cmap=True)
# 220:蓝色(负相关), 10:红色(正相关)
# as_cmap=True:返回colormap对象

# ==================== 绘制热力图 ====================
sns.heatmap(
    corr_matrix,  # 相关系数矩阵
    annot=True,  # 显示数值标注
    cmap=cmap,  # 配色方案
    vmax=1,  # 最大值为1
    vmin=-1,  # 最小值为-1
    center=0,  # 中心值为0
    square=True,  # 格子为正方形
    linewidths=.5,  # 格子线宽0.5
    cbar_kws={"shrink": .5},  # 颜色条缩小为50%
    ax=ax,  # 在指定子图上绘制
    fmt='.3f',  # 数值格式:保留3位小数
    annot_kws={'size': 12}  # 标注字体大小12
)

ax.set_title("A股主要指数相关性矩阵\n(2022-2023年对数收益率)", fontsize=14)
# 设置标题,包含时间范围说明
plt.tight_layout()  # 自动调整布局
plt.show()  # 显示图形

# 图形解读:
# 颜色越红,相关性越强(接近1)
# 颜色越蓝,负相关性越强(接近-1)
# 白色表示相关性接近0
# 通常A股指数间高度正相关(0.7-0.95)

代码深度解析:

  1. 配色方案选择:
    • diverging_palette: 发散配色(适合有正负的数据)
    • center=0: 强调正负相关对比
    • annot=True: 显示数值便于精确解读
  2. 参数说明:
    • square=True: 保持格子为正方形
    • fmt='.3f': 保留三位小数
    • cbar_kws: 颜色条设置

34.4.2 时间序列对比

列表 34.4
# 注:该代码块依赖的数据来自上方平台任务代码块,因其未执行,本块也无法执行

# ==================== 计算累计收益率 ====================
# 累计收益率表示从起点投资的收益
cum_returns = (1 + returns).cumprod() - 1
# (1 + returns): 1 + 收益率 = 价格比
# .cumprod(): 累计乘积(连续复利)
# - 1: 转换为收益率形式

# ==================== 创建子图 ====================
fig, axes = plt.subplots(2, 1, figsize=(14, 10))
# 2行1列的子图,总大小14x10英寸

# ==================== 子图1:每日收益率 ====================
returns.plot(ax=axes[0], linewidth=1)
# 在第一个子图上绘制收益率曲线
axes[0].set_title('每日收益率', fontsize=14)  # 设置标题
axes[0].set_ylabel('收益率', fontsize=12)  # y轴标签
axes[0].legend(fontsize=10)  # 显示图例
axes[0].grid(True, alpha=0.3)  # 添加网格线
axes[0].axhline(y=0, color='k', linestyle='--', linewidth=0.8)
# 添加y=0的参考线,黑色虚线

# ==================== 子图2:累计收益率 ====================
cum_returns.plot(ax=axes[1], linewidth=2)
# 在第二个子图上绘制累计收益率曲线
axes[1].set_title('累计收益率', fontsize=14)  # 设置标题
axes[1].set_xlabel('日期', fontsize=12)  # x轴标签
axes[1].set_ylabel('累计收益率', fontsize=12)  # y轴标签
axes[1].legend(fontsize=10)  # 显示图例
axes[1].grid(True, alpha=0.3)  # 添加网格线
axes[1].axhline(y=0, color='k', linestyle='--', linewidth=0.8)
# 添加y=0的参考线

plt.tight_layout()  # 自动调整布局
plt.show()  # 显示图形

# ==================== 统计分析 ====================
print('\n收益率统计:')
for col in returns.columns:  # 遍历每个指数
    print(f'\n{col}:')
    # 年化收益率 = 日均值收益率 * 252个交易日
    print(f'  年化收益率: {returns[col].mean() * 252:.4f}')
    # 年化波动率 = 日标准差 * sqrt(252)
    print(f'  年化波动率: {returns[col].std() * np.sqrt(252):.4f}')
    # 夏普比率 = (收益率 - 无风险利率) / 波动率
    print(f'  夏普比率(假设无风险利率3%): {(returns[col].mean() * 252 - 0.03) / (returns[col].std() * np.sqrt(252)):.4f}')
    # 夏普比率衡量单位风险的超额收益,越高越好

34.4.3 滚动相关性分析

金融应用:相关性的不稳定性

相关系数不是恒定的,它会随时间变化:

  1. 市场状态依赖:
    • 牛市:股票普遍同涨,相关性高
    • 熊市:恐慌抛售,相关性极高(相关性崩塌)
    • 震荡市:个股分化,相关性降低
  2. 危机期间:
    • 2008金融危机:所有资产相关性趋近1
    • 分散化失效
    • 对冲困难
  3. 策略启示:
    • 不能依赖历史相关性
    • 需要动态监控相关性
    • 极端事件时相关性可能逆转
列表 34.5
# 注:该代码块依赖的数据来自上方平台任务代码块,因其未执行,本块也无法执行

# ==================== 选择分析对象 ====================
# 选择上证综指和深证成指进行滚动相关性分析
index1 = 'SH'  # 上证综指
index2 = 'SZ'  # 深证成指

# ==================== 计算滚动相关性 ====================
window = 60  # 滚动窗口大小:60个交易日(约3个月)
rolling_corr = returns[index1].rolling(window).corr(returns[index2])
# .rolling(window):创建滚动窗口
# .corr():计算窗口内的相关系数

# ==================== 创建子图 ====================
fig, axes = plt.subplots(2, 1, figsize=(14, 10))

# ==================== 子图1:滚动相关性 ====================
axes[0].plot(rolling_corr.index, rolling_corr, linewidth=2, label='滚动相关性')
# 绘制滚动相关性曲线
axes[0].axhline(y=0.8, color='r', linestyle='--', linewidth=1, label='高度相关阈值0.8')
# 添加高度相关阈值线
axes[0].axhline(y=0.5, color='orange', linestyle='--', linewidth=1, label='中度相关阈值0.5')
# 添加中度相关阈值线
axes[0].set_title(f'{index1}{index2}的滚动相关性(窗口={window}日)', fontsize=14)
axes[0].set_ylabel('相关系数', fontsize=12)
axes[0].legend(fontsize=10)
axes[0].grid(True, alpha=0.3)

# ==================== 子图2:指数走势(归一化) ====================
# 将指数归一化到100,便于比较走势
norm_prices = (data[[index1, index2]] / data[[index1, index2]].iloc[0]) * 100
# 除以初始值再乘100,使起点为100
axes[1].plot(norm_prices.index, norm_prices[index1], label=index1, linewidth=2)
axes[1].plot(norm_prices.index, norm_prices[index2], label=index2, linewidth=2)
axes[1].set_title('归一化指数走势', fontsize=14)
axes[1].set_ylabel('初始价格=100', fontsize=12)
axes[1].legend(fontsize=10)
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# ==================== 统计分析 ====================
print(f'\n滚动相关性分析:')
print(f'平均相关性: {rolling_corr.mean():.4f}')
print(f'相关性标准差: {rolling_corr.std():.4f}')
print(f'相关性范围: [{rolling_corr.min():.4f}, {rolling_corr.max():.4f}]')

# ==================== 相关性稳定性分析 ====================
high_corr_days = (rolling_corr > 0.8).sum()  # 统计高度相关的天数
total_days = rolling_corr.dropna().shape[0]  # 有效天数
print(f'\n高度相关天数(>0.8): {high_corr_days}天 ({high_corr_days/total_days*100:.1f}%)')
# 输出解读:了解两个指数在多大程度上保持高度相关

34.5 投资组合应用

34.5.1 投资组合风险计算

理论背景:马科维茨投资组合理论

哈里·马科维茨(Harry Markowitz, 1952)提出现代投资组合理论(MPT),其核心公式:

\[ \sigma_p^2 = \mathbf{w}^T \mathbf{\Sigma} \mathbf{w} = \sum_{i=1}^{n}\sum_{j=1}^{n} w_i w_j \sigma_i \sigma_j \rho_{ij} \]

其中: - \(\mathbf{w}\): 权重向量 - \(\mathbf{\Sigma}\): 协方差矩阵 - \(\sigma_i, \sigma_j\): 资产标准差 - \(\rho_{ij}\): 相关系数

关键洞见: 1. 分散化效应: - 当相关系数<1时,组合风险<加权平均风险 - 相关系数越低,分散化效果越好 - 完全负相关(ρ=-1)可完全消除风险

  1. 有效前沿:
    • 给定风险下收益最大的组合
    • 给定收益下风险最小的组合
    • 通过优化权重得到
列表 34.6
import numpy as np  # 导入NumPy数值计算库
import matplotlib.pyplot as plt  # 导入Matplotlib绑图库

# ==================== 定义三种相关性场景 ====================
# 比较不同相关系数对组合风险的影响
scenarios = [
    {'name': '低相关(ρ=0.3)', 'corr': 0.3},  # 低相关性,分散化效果好
    {'name': '中度相关(ρ=0.7)', 'corr': 0.7},  # 中度相关性
    {'name': '高相关(ρ=0.95)', 'corr': 0.95}  # 高相关性,分散化效果差
]

# ==================== 假设两只资产的参数 ====================
sigma1 = 0.20  # 资产1年化波动率20%
sigma2 = 0.25  # 资产2年化波动率25%
w1 = 0.5       # 资产1权重50%
w2 = 0.5       # 资产2权重50%

print('投资组合风险分析:')
print(f'资产1波动率: {sigma1:.2%}')
print(f'资产2波动率: {sigma2:.2%}')
print(f'等权重配置(w1={w1}, w2={w2})\n')

# ==================== 计算不同相关性下的组合风险 ====================
results = []
for scenario in scenarios:
    rho = scenario['corr']  # 相关系数

    # 组合方差公式: σ² = w₁²σ₁² + w₂²σ₂² + 2w₁w₂σ₁σ₂ρ
    portfolio_var = (w1**2) * (sigma1**2) + (w2**2) * (sigma2**2) + 2 * w1 * w2 * sigma1 * sigma2 * rho
    portfolio_std = np.sqrt(portfolio_var)  # 组合波动率 = sqrt(组合方差)

    # 加权平均波动率(不考虑相关性的简单平均)
    weighted_avg_std = w1 * sigma1 + w2 * sigma2

    # 风险降低百分比
    risk_reduction = (weighted_avg_std - portfolio_std) / weighted_avg_std

    results.append({
        '场景': scenario['name'],
        '相关系数': rho,
        '组合波动率': portfolio_std,
        '加权平均波动率': weighted_avg_std,
        '风险降低': risk_reduction
    })

    print(f"{scenario['name']}:")
    print(f"  组合波动率: {portfolio_std:.4f} ({portfolio_std:.2%})")
    print(f"  加权平均波动率: {weighted_avg_std:.4f} ({weighted_avg_std:.2%})")
    print(f"  风险降低: {risk_reduction:.2%}\n")

# ==================== 可视化对比 ====================
fig, ax = plt.subplots(figsize=(10, 6))

scenarios_names = [r['场景'] for r in results]  # 场景名称
portfolio_stds = [r['组合波动率'] for r in results]  # 组合波动率
weighted_stds = [r['加权平均波动率'] for r in results]  # 加权平均波动率

x = np.arange(len(scenarios_names))  # x轴位置
width = 0.35  # 柱状图宽度

bars1 = ax.bar(x - width/2, weighted_stds, width, label='加权平均波动率', color='coral', alpha=0.7)
# 蓝色柱:不考虑相关性的波动率
bars2 = ax.bar(x + width/2, portfolio_stds, width, label='组合波动率', color='steelblue', alpha=0.7)
# 橙色柱:考虑相关性的实际波动率

ax.set_title('相关性对投资组合风险的影响', fontsize=14)
ax.set_ylabel('年化波动率', fontsize=12)
ax.set_xticks(x)  # 设置x轴刻度位置
ax.set_xticklabels(scenarios_names)  # 设置x轴刻度标签
ax.legend(fontsize=10)
ax.grid(axis='y', alpha=0.3)

# ==================== 添加数值标签 ====================
for bars in [bars1, bars2]:  # 遍历两组柱状图
    for bar in bars:  # 遍历每根柱子
        height = bar.get_height()  # 获取柱子高度
        ax.text(bar.get_x() + bar.get_width()/2., height,
                f'{height:.2%}',  # 格式化为百分比
                ha='center', va='bottom', fontsize=10)

plt.tight_layout()
plt.show()

# 图形解读:
# 蓝色柱代表不考虑相关性的简单加权平均
# 橙色柱代表考虑相关性后的实际组合风险
# 两者差距越大,分散化效果越好

34.5.2 最优配置权重

列表 34.7
# 注:该代码块依赖的数据来自上方平台任务代码块,因其未执行,本块也无法执行

# ==================== 计算协方差矩阵 ====================
cov_matrix = returns.cov() * 252  # 年化协方差矩阵
# returns.cov(): 日协方差矩阵
# * 252: 年化(假设一年252个交易日)

print('年化协方差矩阵:')
print(cov_matrix)

# ==================== 计算全局最小方差组合 ====================
# Global Minimum Variance Portfolio (GMVP)
# 公式: w = Σ^(-1) * 1 / (1' * Σ^(-1) * 1)
# 其中: Σ是协方差矩阵, 1是全1向量

# 逆协方差矩阵
cov_inv = np.linalg.inv(cov_matrix)
# np.linalg.inv(): 矩阵求逆

# 全1向量
ones = np.ones(len(cov_matrix))
# 创建长度为资产数量的全1向量

# 计算权重
weights_gmvp = np.dot(cov_inv, ones) / np.dot(np.dot(ones.T, cov_inv), ones)
# 分子: Σ^(-1) * 1
# 分母: 1' * Σ^(-1) * 1 (标量)
# 结果归一化,使权重和为1

# ==================== 输出权重 ====================
print('\n全局最小方差组合权重:')
for i, col in enumerate(cov_matrix.columns):
    print(f'{col}: {weights_gmvp[i]:.4f}')
# 输出解读:展示各指数的最优配置权重
# 权重之和为1,权重可正可负(负值表示做空)

# ==================== 计算组合表现 ====================
expected_returns = returns.mean() * 252  # 年化期望收益
portfolio_return = np.dot(weights_gmvp, expected_returns)
# 组合收益 = w' * μ (权重向量点乘收益向量)
portfolio_var = np.dot(weights_gmvp.T, np.dot(cov_matrix, weights_gmvp))
# 组合方差 = w' * Σ * w (二次型)
portfolio_std = np.sqrt(portfolio_var)  # 组合波动率

print(f'\n全局最小方差组合表现:')
print(f'年化期望收益率: {portfolio_return:.4f} ({portfolio_return:.2%})')
print(f'年化波动率: {portfolio_std:.4f} ({portfolio_std:.2%})')
print(f'夏普比率(假设无风险利率3%): {(portfolio_return - 0.03) / portfolio_std:.4f}')

# ==================== 对比等权重组合 ====================
equal_weights = np.ones(len(cov_matrix)) / len(cov_matrix)
# 等权重:每个资产配置1/n
equal_return = np.dot(equal_weights, expected_returns)
equal_var = np.dot(equal_weights.T, np.dot(cov_matrix, equal_weights))
equal_std = np.sqrt(equal_var)

print(f'\n等权重组合表现:')
print(f'年化期望收益率: {equal_return:.4f} ({equal_return:.2%})')
print(f'年化波动率: {equal_std:.4f} ({equal_std:.2%})')
print(f'夏普比率(假设无风险利率3%): {(equal_return - 0.03) / equal_std:.4f}')

# ==================== 风险对比 ====================
print(f'\n风险对比:')
print(f'波动率降低: {(equal_std - portfolio_std) / equal_std:.2%}')
# 输出解读:最小方差组合相比等权重组合的风险降低程度