
用 Python 揭秘均值回归策略:你的收益从何而来?
2026年重磅升级已全面落地!欢迎加入专注财经数据与量化投研的【数据科学实战】知识星球!您将获取持续更新的《财经数据宝典》与《量化投研宝典》,双典协同提供系统化指引;星球内含 500 篇以上独有高质量文章,深度覆盖策略开发、因子分析、风险管理等核心领域,内容基本每日更新;同步推出的「量化因子专题教程」系列(含完整可运行代码与实战案例),系统详解因子构建、回测与优化全流程,并实现日更迭代。我们持续扩充独家内容资源,全方位赋能您的投研效率与专业成长。无论您是量化新手还是资深研究者,这里都是助您少走弯路、事半功倍的理想伙伴,携手共探数据驱动的投资未来!
你有没有想过这样一个问题:今天比特币涨了,明天是更可能继续涨,还是更可能跌回去?
这背后其实藏着一个金融领域的核心命题——时间序列到底有没有「记忆」。传统的统计方法在面对带有趋势、非平稳的真实金融数据时常常力不从心,而去趋势波动分析(Detrended Fluctuation Analysis,简称 DFA)正是为解决这一难题而生的利器。
DFA 被广泛应用于金融、生物、物理等多个领域,它最大的优势在于:即使数据存在趋势和非平稳性,它依然能稳健地刻画出序列内部的长程相关性。本文将带你用 Python 一步步实现 DFA,并以真实的比特币数据为例,亲手算出它的「记忆指数」。
DFA 的核心目标是识别非平稳时间序列的标度特性(scaling properties),也就是观察数据的波动如何随着时间尺度的变化而变化。
整个算法可以拆解为四个关键步骤:
下面我们逐一拆解。
第一步是计算时间序列的累积和,这一步会先把均值从数据中扣除。其数学表达式为:
y(t) = Σ (xᵢ − ⟨x⟩),其中 i 从 1 到 t
这里的 表示整个序列的均值。通过累积求和,原本围绕均值波动的序列被转换成了一条类似「随机游走」轨迹的曲线,便于后续分析。
import numpy as np
def cumulative_sum(x):
# 先减去均值,再做累积求和,得到「轮廓曲线」
return np.cumsum(x - np.mean(x))接下来,我们把累积和序列切分成若干个长度为 的互不重叠的分段,并对每个分段单独做一次线性回归,用拟合出来的直线去除该段的趋势。
对于每一个分段:
去趋势后的残差为:
x(t) = X(t) − Y(t)
然后计算该分段相对于局部趋势的均方根偏差,得到这一段的波动值 :
F(n, i) = sqrt( (1/n) · Σ (xₜ − Yₜ)² )
import numpy as np
from numpy.lib.stride_tricks import as_strided
def calc_rms(x, scale):
# 将序列切成 (段数, scale) 的二维视图,每行就是一个分段
shape = (x.shape[0] // scale, scale)
X = as_strided(x, shape=shape)
scale_ax = np.arange(scale) # 分段内的横坐标 0,1,2,...
rms = np.zeros(X.shape[0]) # 用于存放每段的均方根偏差
for e, xcut in enumerate(X):
coeff = np.polyfit(scale_ax, xcut, 1) # 一阶线性回归,得到趋势
xfit = np.polyval(coeff, scale_ax) # 计算拟合直线上的值
rms[e] = np.sqrt(np.mean((xcut - xfit) ** 2)) # 去趋势残差的 RMS
return rms小贴士:这里用
as_strided是为了高效切分数据,但它直接操作内存视图,使用时务必保证scale能整除数据长度,否则可能读到越界数据。对新手而言,用普通切片循环更安全。
把同一个尺度 下所有分段的波动值汇总,再取均方根,就得到了该尺度对应的总体波动函数 :
F(n) = sqrt( (1 / (N/n)) · Σ F(n, i)² )
def calculate_fluctuations(y, scales):
fluct = np.zeros(len(scales)) # 存放每个尺度对应的总体波动
for e, sc in enumerate(scales):
# 对每个尺度,计算所有分段 RMS 的均方根,得到总体波动
fluct[e] = np.sqrt(np.mean(calc_rms(y, sc) ** 2))
return fluct最后一步,我们在双对数坐标下,对波动函数与尺度做线性拟合,拟合直线的斜率就是我们梦寐以求的 DFA 标度指数 :
log F(n) = α · log n + β
def dfa(x, scale_lim=[5, 9], scale_dens=0.25, show=False):
y = cumulative_sum(x) # 第一步:累积求和
# 以 2 的幂次生成一系列时间尺度(窗口大小)
scales = (2 ** np.arange(scale_lim[0], scale_lim[1], scale_dens)).astype(int)
fluct = calculate_fluctuations(y, scales) # 计算各尺度的波动函数
# 在双对数坐标下做线性拟合,斜率 coeff[0] 即为标度指数 alpha
coeff = np.polyfit(np.log2(scales), np.log2(fluct), 1)
if show:
plt.loglog(scales, fluct, 'bo')
plt.loglog(scales, 2 ** np.polyval(coeff, np.log2(scales)),
'r', label=r'$\alpha$ = %0.2f' % coeff[0])
plt.title('DFA')
plt.xlabel('log10(time window)')
plt.ylabel('log10 <F(t)>')
plt.legend()
plt.show()
return scales, fluct, coeff[0]易错点提醒:原始代码中使用了
np.int,但它在较新版本的 NumPy 中已被移除,直接运行会报错。上面的代码已替换为内置的int,这是实际跑通代码时最容易踩的坑之一。
可以看作是经典 Hurst 指数的推广,它的取值直接告诉我们序列的自相关结构:
直觉上理解:因为长度为 的无相关随机游走,其期望位移大致按 增长,所以 恰好对应白噪声;当 落在 0 到 1 之间时,结果对应于分数高斯噪声。
下面把所有模块拼起来,应用到过去 5 年的比特币日线收益率上:
import numpy as np
import matplotlib.pyplot as plt
from numpy.lib.stride_tricks import as_strided
def cumulative_sum(x):
return np.cumsum(x - np.mean(x))
def calc_rms(x, scale):
shape = (x.shape[0] // scale, scale)
X = as_strided(x, shape=shape)
scale_ax = np.arange(scale)
rms = np.zeros(X.shape[0])
for e, xcut in enumerate(X):
coeff = np.polyfit(scale_ax, xcut, 1) # 线性回归求趋势
xfit = np.polyval(coeff, scale_ax)
rms[e] = np.sqrt(np.mean((xcut - xfit) ** 2))
return rms
def calculate_fluctuations(y, scales):
fluct = np.zeros(len(scales))
for e, sc in enumerate(scales):
fluct[e] = np.sqrt(np.mean(calc_rms(y, sc) ** 2))
return fluct
def dfa(x, scale_lim=[5, 9], scale_dens=0.25, show=False):
y = cumulative_sum(x)
scales = (2 ** np.arange(scale_lim[0], scale_lim[1], scale_dens)).astype(int)
fluct = calculate_fluctuations(y, scales)
coeff = np.polyfit(np.log2(scales), np.log2(fluct), 1)
if show:
plt.loglog(scales, fluct, 'bo')
plt.loglog(scales, 2 ** np.polyval(coeff, np.log2(scales)),
'r', label=r'$\alpha$ = %0.2f' % coeff[0])
plt.title('DFA')
plt.xlabel('log10(time window)')
plt.ylabel('log10 <F(t)>')
plt.legend()
plt.show()
return scales, fluct, coeff[0]
if __name__ == '__main__':
import yfinance as yf
# 下载比特币近 5 年的日线数据
data = yf.download('BTC-USD', period='5y')
# 计算每日收益率:(P_t - P_{t-1}) / P_{t-1}
r = data['Close'].diff() / data['Close'].shift(1)
r.dropna(inplace=True)
# 运行 DFA 并绘图
scales, fluct, alpha = dfa(r, show=True)
print("Scales:", scales)
print("Fluctuations:", fluct)
print("DFA Exponent: {}".format(alpha))运行结果:我们得到 。
这个略大于 0.5 的数值意味着比特币的收益率序列表现出持续性(persistent behavior)——也就是说,价格的上涨倾向于被更多的上涨跟随,下跌也倾向于被更多的下跌跟随,趋势具有一定的延续惯性。
延伸思考:0.57 离 0.5 并不算远,说明这种持续性其实相当微弱。在实盘中,单凭一个略大于 0.5 的标度指数就重仓押注趋势是很危险的。DFA 更适合作为研究市场结构、辅助判断策略类型(趋势型 vs 均值回归型)的工具,而非直接的买卖信号。
本文带你完整走过了 DFA 的四个核心步骤:累积求和 → 分段去趋势 → 计算波动函数 → 双对数拟合求标度指数。
核心要点回顾:
掌握 DFA,你就拥有了一把分析时间序列内在结构的趁手工具。不妨把它套用到你关注的股票、外汇或其他资产上,看看它们各自的「记忆」如何。
2026年全面升级已落地!【数据科学实战】知识星球核心权益如下:
星球已沉淀丰富内容生态——涵盖量化文章专题教程库、因子日更系列、高频数据集、PyBroker实战课程、专家深度分享与实时答疑服务。无论您是初探量化的学习者,还是深耕领域的从业者,这里都是助您少走弯路、高效成长的理想平台。诚邀加入,共探数据驱动的投资未来!
好文推荐
1. 用 Python 打造股票预测系统:Transformer 模型教程(一)
2. 用 Python 打造股票预测系统:Transformer 模型教程(二)
3. 用 Python 打造股票预测系统:Transformer 模型教程(三)
4. 用 Python 打造股票预测系统:Transformer 模型教程(完结)
6. YOLO 也能预测股市涨跌?计算机视觉在股票市场预测中的应用
9. Python 量化投资利器:Ridge、Lasso 和 Elastic Net 回归详解
好书推荐