← 返回量化与 AI 投研概念库

相关性:你以为的五个仓位,可能只是一个半

最后更新 2026-08-18
R2 · 注意风险

你手里有五只票。银行、白酒、光伏、医药、一只港股科技——听起来挺散的,行业各不相同,你当时挑的时候还特意错开了。

然后某个周一,五条线一起往下走。不是先后,是同一天、同一个上午、幅度还差不多。

这时候你大概会想:是不是我分散得还不够,再加两只?——这个念头本身,就是这篇要拆的东西。因为你加的不是"数量",你真正需要买的是"不同步"。而"不同步"这件事有一个可以算出来的数字,它叫相关性;更要命的是,这个数字不是常数,它在你最需要它低的时候,往往正在往上跑。

先说人话:它量的到底是什么

你和一个同事都住城东,都在城西上班。你骑电动车走小路,他开车走高架。平时你俩的通勤时间几乎没什么关系——他堵在高架上的那天你可能一路顺畅,你车没电的那天他准点到。看一个月的记录,两个人的迟到日子基本对不上。

那么下暴雨的那天呢?两个人都迟到,而且都迟到很久。

这就是相关性这个概念在说的全部事情:两件事的起落有多同步。注意它衡量的是"同步程度",不是"谁大谁小"。你俩通勤时间可以差二十分钟,只要每天一起变长、一起变短,同步程度就是高的。

它被压缩成一个介于 −1 和 +1 之间的数:

  • +1:完全同步,一个涨另一个必涨,涨跌的节拍一模一样。
  • 0:没什么关系,一个的表现完全帮不上你猜另一个。
  • −1:完全反着来,一个涨另一个必跌。

协方差是它没被压缩之前的样子——同样描述同步程度,但带着两个资产各自波动大小的量纲,两只波动巨大的股票哪怕同步性一般,协方差也可能很大。所以它没法横向比较。相关系数只是把协方差除掉两边各自的波动幅度,让所有配对都落在同一把尺子上。这件事在代码里就是一行,qlib 的风险模型基类里写得很直白:

vola = np.sqrt(np.diag(S))
corr = S / np.outer(vola, vola)

S 是协方差矩阵,对角线上是各自的方差,开方就是各自的波动率;拿协方差去除以两边波动率的乘积,出来的就是相关系数矩阵。协方差和相关系数不是两个东西,是同一个东西的两种刻度。

关于"低相关为什么能把组合波动压下来"的那套数学推导,分散化的数学 已经算得很清楚了,这篇不重复。这篇要讲的是它没讲的三件事:这个数字实际是怎么被算出来的、它为什么会在关键时刻背叛你、以及为什么一个静态的数字远不如一条滚动的曲线。

它是怎么被算出来的:三个决定成败的细节

大部分人以为算相关性就是"把两列数扔进公式"。真到工程里,公式是最不重要的部分,前面三步的处理决定了你得到的数字是真的还是垃圾。

第一步:必须先转成收益率,不能拿价格直接算

这是最常见也最致命的错误。两只长期都在上涨的股票,价格序列的相关系数几乎必然接近 +1——不是因为它们同步,是因为它们都在涨。这个数字里没有任何关于"同步"的信息,它只反映了两条线都有向上的趋势。

正确做法是先把价格转成每日收益率(今天相对昨天变化了百分之几),再算相关性。Vibe-Trading 的跨资产相关性模块就是这么做的,但它在这一步上还踩过一个更隐蔽的坑,注释里专门留了痕迹:

# ``fill_method=None`` is explicit because under the project's
# pandas>=2,<3 pin the ``pct_change`` default forward-fills missing
# prices, silently manufacturing 0% returns on halted sessions.
rets = ts.pct_change(fill_method=None).dropna()

翻译过来:计算涨跌幅的那个函数,在某些版本下遇到缺失的价格会自动用前一天的价格补上。补上的后果是——停牌那几天,这只票被算成了"每天涨跌 0%"。而 0% 是一个不动的数,它会把这段时间的相关性硬生生往 0 拉。于是你看到一个"低相关"的漂亮结果,实际来源是这只票压根没在交易。所以这里必须显式关掉自动填充,宁可少几行数据,也不要凭空造出来的零。(版本相关的默认行为随时可能变,重点是"检查你的涨跌幅是不是被填出来的"这件事本身。)

第二步:对齐。两个资产得说同一个时间

跨市场算相关性时,A 股的日线、美股的日线、加密货币的日线,时间戳压根不在一个刻度上——同一个"某月某日",一个是北京时间收盘,一个是美东时间收盘,落到 UTC 上差着几个小时。不处理的话,两列数会错位一格,算出来的相关性会莫名其妙地偏低。上游的做法是把所有时间戳统一归一到"日"这个粒度,抹掉时分秒,再做对齐。

对齐之后还有一刀:只保留两边都有数据的那些天(内连接后丢弃缺失行)。这一刀很干净,但代价要心里有数——你的样本量会被最短的那个序列截断。拿一只上市三年的票和一只上市二十年的票算相关性,你得到的是三年的相关性,不是二十年的。

第三步:选线性还是选排序

最常用的是皮尔逊相关系数,它测的是线性关系——两个变量能不能用一条直线互相解释。它有个众所周知的软肋:对极端值极其敏感。三年里的某一天两只票都暴跌 15%,这一天的贡献可能顶得上前面几百天,一个数据点就能把整条结论掰弯。

另一个选项是斯皮尔曼相关系数,它不看数值本身,只看排名——把两列收益各自从小到大排序,然后比较排名的一致性。这样一来,暴跌那天只是"排在最后一名",不再是一个能压塌天平的巨大数字。上游把这两种做成了可切换的参数(methodpearsonspearman,当前默认前者,可配置)。

两者的分工可以这么记:想知道"跌 1% 时另一个大概跌多少",用皮尔逊;想知道"它们是不是总在同一批日子表现差",用斯皮尔曼。结论对这个选择很敏感,报一个相关系数却不说是哪种,这个数字的可信度要打折。

还有一处值得学的防御

qlib 的滚动相关性算子里有一段很短但很聪明的保护:如果窗口内某一边的标准差接近 0,结果直接置为空值。

res.loc[
    np.isclose(series_left.rolling(self.N, min_periods=1).std(), 0, atol=2e-05)
    | np.isclose(series_right.rolling(self.N, min_periods=1).std(), 0, atol=2e-05)
] = np.nan

道理是:相关系数的分母是两边的波动率乘积,一边几乎不动的时候分母趋近于零,算出来的数会剧烈失真甚至没有意义(一条平线和任何东西的"同步程度"都无从谈起)。长期停牌、极端低流动的品种,最容易触发这种情况。宁可返回"算不出来",也不返回一个假装算得出来的数字。

顺带一提反面教材:也有实现在算不出来时直接把结果填成 0.0。这个选择很危险——0 在相关性里不是"未知",是"不相关"这个明确的好消息。 把未知记成好消息,等于给你的组合发了一张伪造的健康证明。

为什么危机那天,所有东西的相关性都往 1 跑

这是相关性最反直觉、也最贵的一课:你买的分散化,在最平静的日子最有效,在最需要它的日子最无效。

它不是玄学,可以拆成三层机制。

第一层:共同因子的方差暴涨,把特质波动稀释掉了。

把一只股票的收益粗略拆成两部分:一部分跟着整个市场走(乘上它对市场的敏感度),另一部分是它自己的事——业绩、公告、行业新闻。平常日子里,第二部分占大头,所以两只股票看上去各走各的,相关性可能只有 0.3。

危机来的时候,市场那一部分的波动被放大了好几倍,而"它自己的事"那部分并没有同步放大——公司还是那个公司。于是在总波动里,共同的那块占比急剧上升,各自独有的那块被挤到边缘。相关性是"共同的部分 ÷ 总波动",分子暴涨、分母里独有的成分相对缩水,结果就是相关性机械地往 1 靠。这不是资产变了性质,是市场因子把麦克风抢了。

第二层:卖出的顺序不由基本面决定,由流动性决定。

被迫减仓的时候,机构卖的不是"最该卖的",而是"卖得掉的"。杠杆被追缴、赎回压力上来,第一批被抛的往往是流动性最好的资产——哪怕它基本面毫无问题。于是本来八竿子打不着的两类资产,因为被同一批人同时持有、同时抛售,在那几天变成了高度相关。这一层跟基本面无关,跟持有人结构有关。这也是 因子拥挤 那篇要讲的同一件事的另一个切面:相关性有一部分是被资金流造出来的,不是被资产本身的属性决定的。

第三层(容易被忽略):有一部分"危机相关性上升"是统计假象。

假设两个资产的真实关系一整年恒定不变,你现在只挑出"跌得最凶的那 20 天"来算相关性——算出来的数会天然高于全年的值。原因是你按"波动大"这个条件筛了样本,而在只剩大波动的子样本里,共同因子的占比本来就更高。换句话说,条件在极端行情上计算的相关系数,天生偏高。

这一层不是要你放心,恰恰相反:它意味着"危机相关性趋同"这个现象里,一部分是统计效应、一部分是真实变化,而这两者用肉眼分不开。所以那些说"我们模型里对危机相关性做了修正"的说法,你至少要问一句修正的是哪一部分。诚实的态度是:承认这个数字在极端区间不可靠,然后不要把仓位安全建立在它身上。真正兜底的是 尾部风险 那一层的准备,不是相关性矩阵。

🚧 避坑

最贵的误读:把"历史相关性低"当成"未来会分开跌"。 相关性是描述过去的统计量,不是承诺。你在平静期算出的 0.2,是市场因子沉睡时的读数;市场因子醒来那天,它可能是 0.85。用平静期的相关性去决定杠杆和仓位,等于用晴天的路况规划暴雨天的通勤时间。

滚动相关性:为什么一个数字远不如一条曲线

如果你只从上面记住一件事,请记这个:别用一个静态的相关系数。

"过去五年这两个资产的相关性是 0.3"——这句话把一个明显在变的东西当成了常数。同样是这五年的数据,如果你改成每次只用最近一段时间(比如最近 60 个交易日)算一次,然后逐日往前推,你得到的不是一个数,是一条起伏的曲线。这条曲线通常长这样:大部分时间在 0.2 到 0.4 之间晃,然后在某几段急速抬升到 0.8 以上,再慢慢回落。

那个"0.3"是这条曲线的平均值。而你的账户不活在平均值里,它活在曲线的每一天,尤其活在最高的那几天。

滚动的做法在工程上就是加一个窗口参数:只取最近若干行数据来算(上游模块把它做成了可配置的回看天数,当前默认取近三个月这个量级)。qlib 那边则把它做成了表达式里的算子,写 Corr(A, B, N) 就是"用最近 N 期滚动地算 A 和 B 的相关性",N 由你定。

但滚动窗口引入了一个逃不掉的取舍,值得单独说:

  • 窗口开得短:反应快,相关性一变你马上看得见;代价是噪声极大,数字天天乱跳,你会被一堆假警报折磨,甚至因为几天的巧合就动了仓位。
  • 窗口开得长:曲线平滑好看,但反应慢。等你看到相关性抬头,那波共振已经在你账户里发生完了。

这个取舍没有正确答案,它和参数选择的所有麻烦是同一类问题——你在做的是估计误差与滞后之间的交换,不存在两头都要的窗口长度。实践中比较稳的做法是同时看长短两条,短的当警报,长的当基准,两条开始收口的时候才是真信号。这和参数高原那一套思路是一致的:不要去找"最优"的那个窗口值,要看结论在一片窗口范围内稳不稳。

滚动相关性最好的用法,也不是拿它来预测。是拿它来体检:定期看一眼你自己持仓两两之间的滚动相关性,找那些正在悄悄合并的曲线。当你发现两个你一直当成"不同赌注"的仓位,最近三个月的相关性已经从 0.2 爬到 0.7,那么你实际承担的风险,早就不是你以为的那个了。

系统层:为什么专业系统要给相关性矩阵"动手术"

再往深一层,问题会变得更麻烦。两个资产算一个相关性还好;一百个资产,就是四千九百多个配对,每一个都是一个需要从有限历史里估出来的数。

参数的数量随资产数量平方增长,样本量却是有限的。结果是这个矩阵里塞满了估计误差,而且这些误差不是随机地互相抵消——把它直接送进组合优化器,优化器会专门去找那些估计误差最大的配对,因为在数字上它们看起来是最好的对冲。你得到一个在历史上完美、在未来一碰就碎的配置。

所以专业的风险模型基本都不直接用原始样本协方差矩阵。qlib 里给了两条主流的路:

一条是"收缩"。 把样本算出来的矩阵,和一个结构简单、几乎没有估计误差的目标矩阵按比例混合:

S_hat = (1 - alpha) * S + alpha * F

F 这个目标可以是"假设所有资产等方差、两两零相关",也可以是"保留各自的方差、但假设两两相关系数全都等于一个平均值",还可以是"假设背后只有单一因子"。这几个目标都笨,但笨有笨的好处——它们没有估计误差。混合的比例既可以手工给,也可以用统计方法自动定。这个操作的本质是:主动往数据里掺一点偏见,换取稳定性。 你损失了一点对历史的贴合度,换来的是这个矩阵不会因为多了两个月数据就面目全非。

另一条是"结构化"。 不再逐对估计,而是假设所有资产的收益背后由少数几个共同因子驱动,先估因子、再由因子反推出整个协方差矩阵:

cov(X.T) = F @ cov(B.T) @ F.T + diag(var(U))

参数量一下子从"资产数的平方"降到"资产数乘因子数",估计误差随之骤降。代价是你相信了那个假设——因子模型解释不掉的那部分相关,在这个矩阵里被当成了零。 而危机时新冒出来的共同驱动力,往往恰恰是模型里没有的那个因子。这条路和 风险平价 那类按风险分配权重的方法是配套的:那类方法吃的就是协方差矩阵,矩阵不稳,权重就跟着乱跳。

还有一个细节值得记:处理缺失值的方式会影响矩阵的性质。上游把它做成了三个选项——忽略、填零、遮罩。选遮罩时,代码注释里明确写了每一对资产的有效样本数是不同的(each pair has distinct number of samples)。这意味着矩阵的不同位置由不同长度的历史算出来,拼在一起未必还满足数学上该有的性质,后续做矩阵分解时可能直接报错或给出荒唐结果。缺失值的处理不是数据清洗的小事,它会一路传导到风险模型的可用性。

常见误用

用价格算,不用收益率算。 前面说过,两条上涨曲线的相关系数接近 1 是废话,不是发现。看到任何相关性结论,先问一句算的是什么序列。

把相关当因果。 两个资产同步,可能是因为受同一个宏观变量驱动,也可能纯属巧合。样本越短、配对越多,纯属巧合的概率越高——在几千个配对里挑出相关性最高的那几对,你挑出来的多半是噪音。

忘了它只测线性关系。 有些收益结构天生非线性:平时稳稳赚小钱,极端行情一次性亏大钱。这类东西之间的皮尔逊相关系数可能长期趴在 0 附近,看上去互不相干,但它们的亏损日期高度重合。相关系数为 0 不等于独立,更不等于尾部独立。这是分散化最容易被骗的地方。

用相关性衡量"跟大盘的关系"时和 beta 混淆。 相关性只说同步程度,beta 还带上了"幅度放大多少倍"。相关性 0.9 但幅度只有大盘一半的资产,和相关性 0.9 幅度是大盘两倍的资产,风险完全不同。两者的分界见 beta 与 alpha

报一个数字不报样本区间和窗口。 "它俩相关性 0.3"这句话在没有说明用了哪段历史、多长窗口、哪种方法之前,信息量接近于零。

小结

  • 相关性量的是两个资产起落有多同步,被压缩到 −1 到 +1 之间;协方差是它没除以波动率之前的样子,两者是同一个东西的两种刻度。
  • 算之前的三步比公式重要:必须转成收益率(用价格算是废话)、必须对齐时间(跨市场尤其)、要明确用线性还是用排名(皮尔逊怕极端值,斯皮尔曼稳但丢幅度信息)。停牌被自动填成 0% 涨跌,会伪造出漂亮的低相关。
  • 危机时相关性趋近 1 有三层原因:市场因子的波动暴涨稀释了各自的特质波动、被迫减仓时按流动性而非基本面卖出、以及只在极端子样本上计算本身就会抬高读数。分散化在最需要它的时候最弱,这是结构性的,不是运气差。
  • 静态的一个相关系数是危险的,它是一条起伏曲线的平均值,而你活在曲线的每一天。改用滚动窗口,长短两条一起看,把它当组合体检工具而不是预测工具。
  • 资产一多,相关性矩阵里塞满估计误差,优化器会专挑误差最大的配对下注。专业系统靠收缩(掺入简单结构换稳定)或因子分解(降参数量)来动手术,代价都是主动接受一部分偏见

一句话记住这篇:你手里有几个仓位不重要,重要的是它们会不会在同一天让你难受——而这件事,只有在难受的那天才验得出来。

本文所引源码均来自上述快照版本,是阅读代码得出的机制说明,不是运行结果;具体行为请以你手上那个版本为准。回 量化与 AI 投研概念库 看其它概念各自在说什么;本站内容为投资教育,不构成任何投资建议,边界见 免责声明

内容有错、看不懂、或想看下一篇?告诉我们 →

本文为投资教育整理,不构成任何投资建议;不荐个股/基金、不预测点位、不承诺收益。关键数据与结论请结合权威来源自行验证,并见风险免责声明