【Python量化波动率实战 05】你的组合真的分散了吗:滚动相关与分散化收益

0 阅读7分钟

上一篇的分组实验提醒我们:小样本里个股极端值能吞掉分组效应。这篇换个更贴近日常的问题——你买了五只股票,就真的分散了吗?

"分散"最容易被高估。五只同涨同跌的股票,名义上五份钱,风险上其实是一份。今天用三个指标把"分散"量化:平均两两相关、有效 N、分散化比率,并看看行情变坏时它们怎么变。


本文你将得到什么

  1. 三个分散化指标的定义与 Python 实现:平均两两相关、有效 N、分散化比率(DR)。
  2. 五只股票等权组合的实测:名义 5 只实际只顶 3.92 只。
  3. 一个关键发现:高波动期相关性上升,分散效果缩水——"越跌越 related"。
  4. 一套可复用的滚动监控框架,随时给自己的组合打分。

一、三个指标,一句话一个

  • 平均两两相关 ρ:所有股票对之间相关系数的平均值。越接近 1 越是"一个篮子"。
  • 有效 N:名义 N 只股票在相关性作用下"真正独立"的只数,公式 N / (1 + (N-1)×ρ)。ρ=0 时有效 N=N(完美分散);ρ→1 时有效 N→1(毫无分散)。
  • 分散化比率 DR:成分波动率的加权平均 ÷ 组合实际波动率。DR=1 说明分散没带来任何好处;DR 越大,分散"省下"的波动越多。

三个指标都不需要任何高深模型,一个相关矩阵就够了。


二、先算全样本,再看滚动:波动升、相关升、分散缩

全样本只给你一个平均画像;真正的风险在滚动窗口里——市场恐慌时相关系数集体上移,平时不相关的资产突然步调一致("相关性在危机中上升"是组合管理最著名的坑)。所以我们对每个 20 日窗口重算一遍三个指标,再按组合波动高低分两段对比。


可运行代码

pip install mairui pandas numpy
from mairui import Client
import pandas as pd
import numpy as np
import random

LICENCE = "LICENCE-66D8-9F96-0C7F0FBCD073"  # 演示证书,请换成你自己的正式证书
client = Client(LICENCE)

TRADING_DAYS = 244
CORR_WIN = 20       # 滚动相关窗口
POOL = ["600519", "601318", "600036", "300750", "601088"]
SEED = 20261013


def synth_ohlc(bars, seed):
    # 演示证书返回固定样例数据;用确定性合成路径模拟行情起伏(慢变波动率+波动聚集)。
    # 换成你自己的正式证书后删掉本函数调用即可,其余逻辑零改动。
    rng = random.Random(seed)
    g = 0.0
    out, prev_c = [], None
    for b in bars:
        g = 0.9 * g + rng.gauss(0, 0.25)
        sigma = 0.015 * float(np.exp(g))
        if prev_c is None:
            c = float(b["c"]); o = c * (1 + rng.gauss(0, 0.3 * sigma))
        else:
            c = prev_c * (1 + rng.gauss(0, sigma))
            o = prev_c * (1 + rng.gauss(0, 0.3 * sigma))
        h = max(o, c) * (1 + abs(rng.gauss(0, 0.5 * sigma)))
        l = min(o, c) * (1 - abs(rng.gauss(0, 0.5 * sigma)))
        out.append({"t": b["t"], "c": round(c, 2)})
        prev_c = c
    return out


# 五只股票的日收益矩阵
R = {}
for code in POOL:
    bars = client.stock_history(code, "d", "n", st="20260701", et="20261012")
    bars = synth_ohlc(bars, seed=SEED + int(code))
    closes = pd.DataFrame(bars)["c"]
    R[code] = pd.Series(np.log(closes / closes.shift(1)).to_numpy())
R = pd.DataFrame(R)
N = R.shape[1]


def avg_pairwise_corr(df):
    cm = df.corr().to_numpy()
    return np.nanmean(cm[np.triu_indices(cm.shape[0], k=1)])


# ---- 全样本 ----
rho_full = avg_pairwise_corr(R.dropna())
neff_full = N / (1 + (N - 1) * rho_full)
port = R.mean(axis=1)                       # 等权日再平衡
vol_each = R.std(ddof=1) * np.sqrt(TRADING_DAYS)
vol_port = port.std(ddof=1) * np.sqrt(TRADING_DAYS)
print(f"平均两两相关 {rho_full:.4f} | 有效 N = {N}/(1+{N-1}x{rho_full:.4f}) = {neff_full:.2f}")
print(f"成分平均波动 {vol_each.mean()*100:.2f}% | 组合波动 {vol_port*100:.2f}% | "
      f"DR = {vol_each.mean()/vol_port:.3f}")
print(f"分散化收益 = {(vol_each.mean() - vol_port)*100:.2f} 个百分点/年")

# ---- 滚动 20 日 ----
rows = []
for i in range(CORR_WIN, len(R)):
    win = R.iloc[i - CORR_WIN:i].dropna()
    if len(win) < CORR_WIN:
        continue
    rho = avg_pairwise_corr(win)
    vp = win.mean(axis=1).std(ddof=1) * np.sqrt(TRADING_DAYS)
    dr = win.std(ddof=1).mean() * np.sqrt(TRADING_DAYS) / vp if vp > 0 else np.nan
    rows.append((rho, N / (1 + (N - 1) * rho), vp, dr))
roll = pd.DataFrame(rows, columns=["rho", "neff", "port_vol", "dr"])

# 按组合波动中位数二分:平静期 vs 高波动期
med = roll["port_vol"].median()
calm = roll[roll["port_vol"] <= med]
stress = roll[roll["port_vol"] > med]
print(f"\n滚动窗口 {len(roll)} 个,按组合波动中位数 {med*100:.1f}% 二分:")
print(f"平静期: 平均相关 {calm['rho'].mean():.4f} | 有效 N {calm['neff'].mean():.2f} | DR {calm['dr'].mean():.3f}")
print(f"高波动期: 平均相关 {stress['rho'].mean():.4f} | 有效 N {stress['neff'].mean():.2f} | DR {stress['dr'].mean():.3f}")

说明:演示证书返回固定样例数据,代码用确定性合成路径模拟行情起伏;换成你自己的正式证书后删掉 synth_ohlc 调用即为真实行情,指标逻辑零改动。


运行结果

平均两两相关 0.0691 | 有效 N = 5/(1+4x0.0691) = 3.92
成分平均波动 24.05% | 组合波动 12.60% | DR = 1.908
分散化收益 = 11.44 个百分点/年

滚动窗口 29 个,按组合波动中位数 15.4% 二分:
平静期: 平均相关 0.0582 | 有效 N 4.31 | DR 1.922
高波动期: 平均相关 0.1559 | 有效 N 3.08 | DR 1.688

(示例基于 50 个交易日的演示样本测算,结果为教学演示而非普遍规律;演示证书返回就近样例数据,申请正式证书可拉取全市场全量历史做更长样本验证。)


怎么读这组数字

  1. 名义 5 只,实际 3.92 只:即使全样本相关低到 0.0691(近乎独立),5 只股票的有效独立数量也只剩 3.92。相关哪怕只有一点点,也在悄悄吃掉你的分散。DR=1.908 说明组合波动 12.60% 只有成分均值 24.05% 的一半多一点——分散"省下"了 11.44 个百分点的年化波动,这就是分散化的真实收益。
  2. 高波动期分散效果缩水:相关从 0.0582 升到 0.1559(接近翻 3 倍),有效 N 从 4.31 掉到 3.08,滚动 DR 从 1.922 降到 1.688。翻译成人话:最需要分散的时候(市场大跌),分散给你的保护最薄——因为那时候大家都在一起跌。
  3. 实践含义:评估组合分散度,别只看全样本相关;要看"高波动期的相关"。把本篇的滚动框架放进定时任务,每天刷新一次,相关性恶化会先于亏损出现在看板上——这正是下一篇要做的事。

三个容易踩的坑

  1. 相关矩阵算在价格上而不是收益上:两条都在涨的价格序列相关天然接近 1(伪相关),分散度会被严重低估。永远用收益率(本篇用对数收益)算相关。
  2. 窗口太短,相关系数是噪声:20 日窗口只有 19 个有效点,相关系数的抽样误差很大;单日读数别过度解读,看趋势和分位数更有意义。
  3. 等权 ≠ 分散:本篇用等权是为了把"相关性"这一个变量隔离出来研究。权重本身就是另一个分散维度——五只股票 80% 仓位压在一只上,相关再低也白搭。

小结

分散不是"买了五只"这个动作,而是相关结构决定的数学性质:本样本名义 5 只实际 3.92 只,行情一坏缩到 3.08 只。三个指标(相关、有效 N、DR)把"感觉上挺分散"变成可以每天打分的读数——而读数本身就应该出现在看板上。

下一篇收官:把波动率、回撤、相关性、目标仓位四块仪表拼成一张风险看板,越限自动告警,让风险指标从"事后复盘"变成"盘中可见"。

下篇预告:【Python量化波动率实战 06】一键输出风险看板:波动率仪表盘工程化。


免责声明

本文仅为技术方法演示,不构成任何投资建议。文中示例数据仅用于教学,历史表现不代表未来收益。投资有风险,决策需谨慎。

代码与文档可参考 GitHub:github.com/MaiRuiApi