配对交易与协整:手动计算价差和Z分数
了解协整为何比相关性更关键,使用真实收盘价手动计算对冲比率、spread与z分数,并看清这种策略可能失效的原因。
配对交易与协整密切相关:这类交易建立在两只股票价格之间的价差会反复回归稳定水平这一基础上,而协整是用于判断这种价差是否确实存在的统计性质。相关性衡量的是另一种关系。两只股票可能呈现高相关性,却仍会永久背离;两只股票的日度涨跌即使几乎没有同步性,也可能在多年间维持稳定价差。本页将手动构建对冲比率、spread 和 z 分数,使用真实收盘价对这三项指标进行测算,并用同等篇幅说明这种构建方法可能如何失效。
配对交易中的协整是什么?
相关性衡量两只股票每日涨跌幅的同步程度。其取值范围为 -1 至 +1。+1 表示在样本中的每个交易日,两只股票都同步上涨或下跌。但相关性无法说明两者价格之间的距离。
协整描述的正是这种距离。将股票B的价格减去股票A价格乘以一个固定倍数,再观察剩余部分。如果这条剩余序列,也就是 spread,始终处于有限范围内,而不是不断偏离,那么这两只股票就是协整的。两只股票的价格本身可以向任何方向变动,但两者的组合不会如此。
教科书常用的比喻是牵绳的狗。遛狗者和狗都可能沿街走出不可预测的路线,但牵绳会将两者之间的距离限制在一定范围内。相关性问的是两者是否在同一时刻迈步。协整问的是牵绳是否能维持这种距离。
下表衡量了前一种概念,样本为2024年和2025年的五组常见配对。return_corr列是每日涨跌幅的相关性,这通常就是人们所说的“相关性”。price_corr列是收盘价原始水平的相关性。只要两只股票都上涨过,这一数字就几乎总会显得很高。
每个数字背后的完整 SQL
WITH
px AS (
SELECT
ticker,
date,
toFloat64(any(close)) AS close_px
FROM global_markets.stocks_daily_aggs
WHERE ticker IN ('KO', 'PEP', 'HD', 'LOW', 'XOM', 'CVX', 'V', 'MA', 'AAPL', 'MSFT')
AND date BETWEEN '2024-01-01' AND '2025-12-31'
GROUP BY ticker, date
),
rets AS (
SELECT
ticker,
date,
close_px,
close_px / lagInFrame(close_px) OVER (
PARTITION BY ticker ORDER BY date
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW
) - 1 AS ret
FROM px
)
SELECT
p.pair AS pair,
round(corr(a.ret, b.ret), 3) AS return_corr,
round(corr(a.close_px, b.close_px), 3) AS price_corr
FROM
(
SELECT
tupleElement(t, 1) AS leg_a,
tupleElement(t, 2) AS leg_b,
tupleElement(t, 3) AS pair
FROM
(
SELECT arrayJoin([
('KO', 'PEP', 'KO / PEP'),
('HD', 'LOW', 'HD / LOW'),
('XOM', 'CVX', 'XOM / CVX'),
('V', 'MA', 'V / MA'),
('AAPL', 'MSFT', 'AAPL / MSFT')
]) AS t
)
) AS p
INNER JOIN rets AS a ON a.ticker = p.leg_a
INNER JOIN rets AS b ON b.ticker = p.leg_b AND b.date = a.date
WHERE isFinite(a.ret) AND isFinite(b.ret)
GROUP BY pair
ORDER BY return_corr DESC按每日涨跌幅相关性排序,HD / LOW在表中排名第一,为0.868;同期两只股票价格水平的相关性为0.849。在最后一行,AAPL / MSFT的收益相关性为0.479,价格相关性为0.508。这两列都没有回答配对交易者真正关心的问题。它们衡量的都是共同波动,而配对交易需要衡量两者之间的距离。
如何计算对冲比率和价差?
按顺序分五步,处理两组收盘价。只用原生 Python 和 math 这一个导入项即可完成全部计算:无需安装软件包、数据供应商或网络连接。
- 对每个序列进行中心化。
mean_a = sum(a) / len(a),对列表b也进行相同处理。 - 拟合对冲比率。 它是以 A 为自变量、B 为因变量的直线斜率:
beta = sum((a[i] - mean_a) * (b[i] - mean_b) for i in range(len(a))) / sum((x - mean_a) ** 2 for x in a)。该数值表示,对冲一单位 B 所需的 A 的单位数。 - 构建价差。
spread = [b[i] - beta * a[i] for i in range(len(a))]。对于协整交易对,该序列通常围绕某一水平波动,而不是持续趋势运行。 - 取滚动窗口。 使用
w = spread[i - win + 1 : i + 1]时,窗口均值为mu = sum(w) / len(w),样本标准差为sd = math.sqrt(sum((x - mu) ** 2 for x in w) / (len(w) - 1))。“滚动”表示窗口在被评分当日结束,不使用此后的数据。 - 计算得分并输出。
z = (spread[i] - mu) / sd;当abs(z) > 1.5时打印入场信号,达到abs(z) < 0.5后打印退出信号。z 值为 2,表示价差高于自身近期均值两个标准差。
两组硬编码价格列表就足以观察程序运行,win 设为 5,可使示例更易读。实际分析通常使用接近 63 个交易日的数据,约为一个季度。这些阈值,即 1.5 入场、0.5 退出,只是教程之间沿用的惯例。它们不是经过验证的结论,下面各面板中的内容也没有得出这些阈值。
真实价格中的配对交易 z-score 是什么样的?
下面以两只家喻户晓的饮料股 KO 和 PEP 展示同一计算。对冲比率仅根据 2023 年收盘价计算,并在 2024 年和 2025 年期间保持不变;图表覆盖的正是这段时间。若在同一数据样本上拟合斜率,再用该样本进行评分,就会产生回测中的前视偏差。将拟合窗口与评分窗口分开,是防范这一问题成本最低的方法。
每个数字背后的完整 SQL
WITH
daily AS (
SELECT
date,
anyIf(toFloat64(close), ticker = 'PEP') AS pep,
anyIf(toFloat64(close), ticker = 'KO') AS ko
FROM global_markets.stocks_daily_aggs
WHERE ticker IN ('KO', 'PEP')
AND date BETWEEN '2023-01-01' AND '2025-12-31'
GROUP BY date
HAVING pep > 0 AND ko > 0
),
fitted AS (
SELECT covarSamp(pep, ko) / varSamp(ko) AS beta
FROM daily
WHERE date < '2024-01-01'
),
spread AS (
SELECT
daily.date AS date,
daily.pep - fitted.beta * daily.ko AS spread_usd
FROM daily
CROSS JOIN fitted
),
scored AS (
SELECT
date,
(spread_usd - avg(spread_usd) OVER (
ORDER BY date ROWS BETWEEN 62 PRECEDING AND CURRENT ROW))
/ stddevSampStable(spread_usd) OVER (
ORDER BY date ROWS BETWEEN 62 PRECEDING AND CURRENT ROW) AS z
FROM spread
)
SELECT
toString(toMonday(date)) AS week,
round(argMax(z, date), 2) AS z_score
FROM scored
WHERE date >= '2024-01-01'
AND isFinite(z)
GROUP BY week
ORDER BY week该曲线包含 105 个周度读数。窗口起点的 z-score 为 -1.53,终点为 -0.68。应关注整体形态,而不是任何单个点。若一对资产的表现符合策略假设,它会穿越阈值,随后用几周时间回归零附近,然后再次穿越阈值。若一对资产已不再均值回归,z-score 会停留在阈值之外并持续不回归;在图表上,这表现为远离中部位置的一段长时间平坦走势。
协整价差一定会回归吗?
不一定。更可靠的判断方法,是衡量每个起点之后实际发生了什么。下图按当日 z 分数,将 2019 年至 2025 年末的每个交易日分组,然后报告同一价差在二十个交易日后的平均 z 分数。图中使用价格比的对数,而不是拟合价差,因此不存在事后估计的回归模型影响图表。
每个数字背后的完整 SQL
WITH
daily AS (
SELECT
date,
anyIf(toFloat64(close), ticker = 'PEP') AS pep,
anyIf(toFloat64(close), ticker = 'KO') AS ko
FROM global_markets.stocks_daily_aggs
WHERE ticker IN ('KO', 'PEP')
AND date BETWEEN '2018-01-01' AND '2025-12-31'
GROUP BY date
HAVING pep > 0 AND ko > 0
),
spread AS (
SELECT
date,
log(pep / ko) AS log_ratio
FROM daily
),
scored AS (
SELECT
date,
(log_ratio - avg(log_ratio) OVER (
ORDER BY date ROWS BETWEEN 62 PRECEDING AND CURRENT ROW))
/ stddevSampStable(log_ratio) OVER (
ORDER BY date ROWS BETWEEN 62 PRECEDING AND CURRENT ROW) AS z
FROM spread
),
horizon AS (
SELECT
date,
z,
leadInFrame(z, 20) OVER (
ORDER BY date ROWS BETWEEN CURRENT ROW AND 20 FOLLOWING
) AS z_fwd
FROM scored
WHERE isFinite(z)
)
SELECT
multiIf(z < -2, 'z below -2',
z < -1, 'z -2 to -1',
z < 0, 'z -1 to 0',
z < 1, 'z 0 to 1',
z < 2, 'z 1 to 2',
'z above 2') AS z_bucket,
round(avg(z), 2) AS avg_z_start,
round(avg(z_fwd), 2) AS avg_z_20d_later,
count() AS episode_count
FROM horizon
WHERE date >= '2019-01-01'
AND date <= '2025-11-15'
AND isFinite(z_fwd)
GROUP BY z_bucket
ORDER BY avg_z_start从 z below -2 分组开始的交易日,当日平均 z 分数为 -2.42;在 138 个交易日中,同一价差在二十个交易日后的平均 z 分数为 -0.84。另一端的 z above 2 分组起始值为 2.64,二十个交易日后为 0.92。从两端向零靠拢的曲线,正是均值回归形成的形态。
解读时要同时查看样本数量。两端分组包含的交易日最少,而且同一分组内的交易日高度重叠,因此少数持续时间较长的区间贡献了大多数数据行。平均值并不保证任何单笔交易都会如此。
为什么配对交易会失效?
对冲比率并不是常数。 每个日历年重新拟合一次,结果会发生变化。
每个数字背后的完整 SQL
WITH daily AS (
SELECT
date,
anyIf(toFloat64(close), ticker = 'PEP') AS pep,
anyIf(toFloat64(close), ticker = 'KO') AS ko,
anyIf(toFloat64(close), ticker = 'LOW') AS lowes,
anyIf(toFloat64(close), ticker = 'HD') AS hd
FROM global_markets.stocks_daily_aggs
WHERE ticker IN ('KO', 'PEP', 'HD', 'LOW')
AND date BETWEEN '2019-01-01' AND '2025-12-31'
GROUP BY date
HAVING ko > 0 AND pep > 0 AND hd > 0 AND lowes > 0
)
SELECT
toYear(date) AS year,
round(covarSamp(pep, ko) / varSamp(ko), 3) AS pep_on_ko_beta,
round(covarSamp(lowes, hd) / varSamp(hd), 3) AS lowes_on_hd_beta
FROM daily
GROUP BY year
ORDER BY yearPEP 相对于 KO 的斜率在 2.338 至 2019 期间,以及 -0.543 至 2025 期间的测量结果;旁边列出第二组行业配对,供比较。回测如果用整个历史区间拟合一条斜率,并从第一天起按这条斜率交易,就等于给每笔早期交易使用了当时任何人都不可能获得的参数。解决办法并不复杂,而且有效:使用滚动窗口重新拟合,并用当天可获得的参数为每一天评分。可复现的回测能让其他人检查这一纪律是否得到遵守。
公司行动会在一夜之间改写其中一条腿。 four-for-one 拆股会将一股变成四股,并使报价降低至四分之一。如果用未调整价格跨越该日期计算价差,价差会跳到一个从未实际交易过的水平。合并、分拆和指数调整也会以更隐蔽的方式造成同样的问题。应从拆股调整后的价格历史开始,并注意某个代码可能被重新分配给一家完全不同的公司,这正是代码会破坏数据集的原因。
断裂和机会在发生时看起来完全相同。 一组配对历史上出现过的最大 z 分数,可能是它提供过的最佳入场点,也可能是这段关系结束后的第一周。当天看起来都只是价差过度偏离。仓位规模承担了统计数据无法承担的风险:凯利准则仓位 sizing和波动率目标仓位 sizing提供了两种框架,用于决定单个价差最多可以影响账户多少资金;最大回撤则衡量长期处于均值错误一侧会对权益曲线造成什么影响。
滚动 z 分数不是协整检验。 正式检验是存在的。Engle-Granger 程序先运行回归,再检验剩余价差是否平稳;Johansen 检验则将这一思路扩展到两个标的以上。z 分数假定了这些检验所检查的性质。如果价差根本没有稳定均值,它仍会轻易给出很大的数值,因为趋势序列总会远离自身的滚动均值。
数据说明及这些数字未涵盖的内容
相关性面板使用 2024-01-01 至 2025-12-31 的日收盘价,并根据连续收盘价计算收益率,剔除每个标的的第一个交易日。z 分数轨迹仅使用 2023 年的收盘价拟合对冲比率,然后使用 63 个交易日的滚动均值和样本标准差为 2024 年及 2025 年的数据评分,并取每周最后一次读数。均值回归面板载入自 2018 年以来的历史数据,并从 2019 年起报告,因此每个评分日之前都有完整的滚动窗口;该面板在 2025 年 11 月中旬停止,因此样本内每个交易日之后都有二十个交易日。
这些面板都不是交易策略。它们未计入交易成本、买卖价差、空头一侧的借券费、股息或融资成本。数据仅使用收盘价,因此按收盘价标记的入场价,实际上不可能被交易者成交。
常见问题
相关性与协整有什么区别?
相关性衡量两只股票是否在同一天、朝同一方向变动,取值范围为 -1 到 +1。协整衡量的是两者价格之间的距离是否保持在稳定区间内。一对股票可能前者得分很高,却不满足后者。这通常是相关性筛选找出一对股票后,两者仍逐渐背离的原因。
配对交易中的对冲比率如何计算?
将一只股票的价格对另一只股票的价格进行线性回归,并取回归斜率。该斜率表示,为对冲另一只股票一单位的变动,需要持有第一只股票的单位数。对冲比率会随时间变化,因此大多数策略会在滚动窗口上重新拟合,而不是对整个样本期只拟合一次。
配对交易者用什么 z 分数进场和退出?
教程示例通常会在价格偏离滚动均值 1.5 至 2 个标准差附近进场,并在偏离收窄至约 0.5 个标准差时退出。这些数值是早期文章沿用的惯例,并非经过测量得出的最优值。调整这些参数会同时改变交易次数和每笔交易的持仓时间。
两只股票可以在低相关性的情况下实现协整吗?
可以。相关性基于每日变动计算,协整则基于价差水平计算。因此,两只股票的日间变动可能很少同向,但长期价差仍能保持稳定。反过来也可能发生:两只股票的日相关性很高,但价差却逐年扩大。
为什么协整配对会失效?
维持价差稳定的联系可能发生变化。例如,公司合并、分拆、业务构成改变,或指数调整导致股票的持有者结构发生变化。关系破裂前几天的统计特征,可能与普通均值回归前几天的特征相同。因此,交易规模需要为这种风险预留,而不能指望提前预测。
本文每个面板都附有完整且准确的 SQL 查询,因此窗口、拟合周期和分组边界均清晰可见。若要在您关注的一对股票上运行相同的 spread 和 z 分数,请在 Strasmore 终端用自然语言提问。