用拓扑数据分析挖掘量化因子:从持久同调到Python实战
在量化研究里经常遇到这样一类问题日常使用的动量、波动率、均线偏离等因子本质上是价格序列的一阶或二阶统计量一旦行情进入复杂的震荡、趋势切换和量价背离阶段这些统计特征容易钝化。近几年不少量化团队开始尝试用拓扑数据分析TDA从“形状”和“连通性”的角度去刻画行情结构并把提取到的持续性特征作为新的候选因子。本文会围绕这个方向做一次完整的实战拆解从拓扑数据分析的基本概念讲起逐步完成 Python 环境配置、时间序列点云构建、持久图计算、拓扑特征提取以及因子 IC 和分组收益检验适合已经有 Python 基础、想了解 TDA 因子挖掘方法的量化研究者和数据科学从业者。这里先说明一点本文不是论文复现也不是宣称 TDA 因子一定能提升收益而是给出一套可以照着运行、照着扩展的最小闭环方案。读完这篇文章你能够自己把任意一段行情数据转换成拓扑因子并接入已有的多因子分析流程。1. 为什么用拓扑数据分析做行情结构因子挖掘1.1 传统行情因子的局限传统因子大多建立在价格、成交量、持仓量等数据的统计特征上例如动量因子过去 20 日收益率。波动率因子过去 20 日收益率的滚动标准差。均值回归因子当前价格偏离移动平均线的程度。量价相关性因子成交量与价格的相关系数。这类因子有一个共同特点它们描述的是数据的“数值分布”或“时间相关性”很难描述数据作为一种几何对象时的“结构”。比如两个标的过去 20 天的收益率均值、标准差完全一致但一个呈现缓慢爬升趋势另一个呈现反复震荡格局传统统计因子可能把它们看成同一类状态。拓扑数据分析的作用就是把这种“形状差异”变成可计算的数值特征。另一个常见问题是金融时间序列噪声大、非平稳传统特征对异常值敏感。拓扑不变量对连续形变具有不变性这是 TDA 在噪声环境下仍有价值的重要原因。1.2 拓扑数据分析能做什么拓扑数据分析Topological Data Analysis是一种基于代数拓扑和计算几何的数据分析方法。它不直接关心数据的坐标也不关心数据降维到几个主成分而是关注数据点云在多个尺度下的连通分支、环状结构、空洞等拓扑特征。在行情结构因子挖掘中TDA 通常被用来做两件事第一把一维行情序列通过“滑窗嵌入”构造为高维点云再用 TDA 描述这个点云内部的结构。第二计算持久图从图的点分布中提取数值特征例如持久环的数量、寿命、总持久性等这些数值可以作为候选因子进入多因子模型。与 PCA、t-SNE 等降维方法相比TDA 更适合回答“数据里是否存在某种持续存在的结构”这一类问题而不是“数据如何排列在一个平面上”。这一点恰好契合行情结构研究中对“趋势持续性”“震荡循环”“状态切换”等概念的描述需求。1.3 拓扑因子与传统因子的边界拓扑因子并不是要完全替代传统因子。它在实际研究中的角色更像是一种“增量视角”传统因子擅长捕捉线性趋势和波动强度。拓扑因子擅长捕捉循环结构、分离结构和多尺度下的持续性特征。因此一个比较合理的做法是先独立计算拓扑因子再与动量、波动率、流动性等传统因子放在同一个正交化框架里比较观察它是否提供了额外的预测信息。如果拓扑因子与已有因子高度相关那么它的贡献有限如果相关性低且 IC 稳定就需要进一步研究其背后的行情解释。2. 拓扑数据分析的核心概念2.1 点云从行情序列到高维空间拓扑数据分析处理的对象通常是“点云”也就是一组点的集合。行情数据是时间序列不能直接输入 TDA 管线需要先转换成点云。最常见的转换方式是“滑窗嵌入”。假设我们有过去 60 个交易日的收益率序列r_1, r_2, ..., r_60用长度为 10 的窗口、步长为 2 去切分这段序列就能得到一系列子向量[r_1, r_2, ..., r_10] [r_3, r_4, ..., r_12] [r_5, r_6, ..., r_14] ...每个子向量可以看作是 10 维空间中的一个点所有子向量的集合就是一个点云。这样一个时间窗口内的行情状态就被映射成为高维空间中的一块点云。后面的持久同调计算都是在这块点云上进行的。这里需要注意两种滑窗概念的区别第一种是“行情样本的滑窗嵌入”用于把一个序列转成点云本文使用这种方式。第二种是“因子计算的滚动窗口”每隔若干根 K 线滑动一次用于生成时间序列上的因子值。在实战代码中两者会同时出现需要区分清楚。2.2 单纯复形与过滤得到了点云之后TDA 需要在点云上构造一种叫做“单纯复形”的组合结构。简单理解单纯复形是由点、边、三角形、四面体等基本几何单元按规则拼成的结构。最常用的是 Vietoris-Rips 复形简称 Rips 复形。它的构造规则如下当两个点之间的距离小于阈值 ε 时这两个点之间连一条边。当三个点两两之间的距离都小于 ε 时这三个点构成一个三角形。当四个点两两之间的距离都小于 ε 时这四个点构成一个四面体。随着 ε 从 0 逐渐增大点云中会有越来越多的边、三角形和更高维的单纯形出现整个过程称为“过滤”。这个动态扩张的过程是后续计算拓扑特征的核心基础。Rips 复形之所以被广泛使用是因为它只需要点云的距离矩阵计算方式直接适合处理各类点云数据。虽然它的单纯形数量增长很快但配合持久同调的加速算法在中小规模点云上仍然很高效。2.3 持久同调与持久图在过滤过程中点云里的拓扑结构会不断出生和消失0 维拓扑特征代表“连通分量”即相互连接在一起的点的集合。1 维拓扑特征代表“环”或“孔洞”即点云中形成的闭合圈。2 维拓扑特征代表“空腔”类似三维空间中的气泡。每个拓扑结构都有一个“出生尺度”和一个“死亡尺度”。如果某个环在很大尺度范围内始终存在说明它不是一个偶然的噪声结构而可能对应数据中比较显著的循环或周期模式。持久图是记录这些出生和死亡信息的二维图。图中每个点对应一个拓扑特征横坐标是出生尺度。纵坐标是死亡尺度。点离对角线越远说明该结构存在的时间尺度越长越重要。例如H0 持久图上的点通常有一个点是无穷大对应最大的连通分量H1 持久图上的点反映的是点云中显著环的数量和寿命。持久图的价值在于它完整记录了数据在多尺度下的拓扑指纹。接下来提取因子本质上就是把持久图压缩成一个个可用于截面排序的数值指标。2.4 从持久图到拓扑因子持久图本身是二维分布不能直接作为单因子使用还需要进一步特征化。常用特征包括持久环数量有限寿命拓扑特征的数量。平均寿命所有非无穷点的寿命均值。最大寿命寿命最长的特征对应的尺度差。总持久性所有有限寿命之和。各维特征的组合把 H0 和 H1 的特征拼接起来或做组合运算。这些特征可以在每个滚动窗口上计算形成一条因子时间序列。之后再和未来收益做相关分析就完成了“拓扑结构”到“候选因子”的映射。如果希望比较两段行情之间的拓扑差异还可以使用持久图之间的 Bottleneck 距离或 Wasserstein 距离。这会进入更细粒度的行情分类和状态识别场景本文先不做展开。3. 环境准备与依赖安装3.1 运行环境本文示例使用 Python 3.8 及以上版本即可推荐使用 Python 3.9 或 3.10。操作系统可以是 Windows、macOS 或 Linux。建议先在项目目录下创建独立的虚拟环境避免依赖冲突。示例中主要使用以下库numpy数值计算。pandas行情数据和因子处理。scipySpearman 相关系数计算。ripser持久同调计算核心计算库。matplotlib可选持久图可视化。其中 ripser 是 Topological Data Analysis 中最常用的 Python 包之一底层使用 C 实现计算速度较快。另一个常用库是 giotto-tdagtda它提供了更完整的管线封装但依赖相对较重。为了减少环境复杂度和保持代码可读性本文使用 ripser。3.2 安装依赖在项目根目录打开终端使用以下命令安装pip install ripser persim numpy pandas scipy matplotlib如果你的机器上已经配置了 Jupyter Notebook也可以直接在 notebook 中运行以上命令。安装完成后可以用一行代码验证from ripser import Rips import numpy as np print(RipsER version OK)如果安装过程中遇到编译失败问题通常是因为 pip 版本过低、缺少编译器或者系统 Python 版本过旧。建议先把 pip 升级到最新版本再安装pip install --upgrade pip不建议在系统全局环境里直接安装这些科学计算库尤其当系统 Python 被其他项目依赖时很容易出现版本冲突。3.3 一个最小可运行示例安装完依赖后可以先运行下面这个最小示例确认持久同调计算链路正常from ripser import Rips import numpy as np np.random.seed(0) # 生成 80 个三维点 points np.random.normal(size(80, 3)) rips Rips(maxdim1) dgms rips.fit_transform(points) print(H0 点数:, len(dgms[0])) print(H1 点数:, len(dgms[1]))如果能够正常输出 H0 和 H1 的点数说明 ripser 安装成功后续的因子挖掘实验可以在此基础上进行。4. 完整实战从行情序列到拓扑因子为了把前面的概念串起来这一节实现一个完整的因子挖掘流程使用一段模拟行情数据滑窗构建点云计算持久图特征构造拓扑因子并做初步的 IC 和分组收益检验。4.1 实验设计整体流程可以拆成以下五个步骤构造演示行情数据用几何布朗运动生成一段日线价格和收益率序列。对每个分析窗口中的收益率序列做滑窗嵌入构建高维点云。使用 RipsER 计算点云的 H0 和 H1 持久图。从持久图中提取各维度的总持久性、平均寿命、最大寿命等特征。将拓扑特征作为候选因子与未来 20 日收益计算 Spearman IC并查看分组收益。这里使用模拟数据的主要原因是保证代码可复现。换成真实行情数据时只需要把df替换成包含close和ret列的数据框即可。4.2 构建行情数据与滑窗点云先看数据部分。我们生成 1500 个交易日的模拟收盘价和收益率import numpy as np import pandas as pd np.random.seed(42) n_days 1500 rets np.random.normal(0.0003, 0.01, n_days) close 100 * np.cumprod(1 rets) idx pd.date_range(2021-01-04, periodsn_days, freqB) df pd.DataFrame({close: close}, indexidx) df[ret] df[close].pct_change().fillna(0) df.head()这里使用的pct_change()会计算日收益率后面的滑窗嵌入基于收益率而不是收盘价。原因是收益率序列通常更平稳而价格序列带有明显的趋势和量纲直接用价格做距离计算容易被绝对水平主导。接下来定义滑窗嵌入函数把一个等长序列切分成多个高维子向量def build_point_cloud(series, embed_dim10, embed_step2): 将一维序列转换为高维点云。 参数说明 series: 一维收益率序列 embed_dim: 子窗口长度决定点云维度 embed_step: 子窗口采样步长 n len(series) points [ series[i:i embed_dim] for i in range(0, n - embed_dim 1, embed_step) ] # 如果序列长度不够至少保留一个点 if not points: points.append(series[-embed_dim:]) return np.array(points)这里的核心思路是在同一个分析窗口内部把连续的embed_dim个收益率视为一个点然后让窗口在序列上滑动得到一系列点。这些点在embed_dim维空间中形成一块点云后续的拓扑结构就从这块点云中计算。4.3 计算持久图并提取拓扑特征下面定义拓扑特征提取函数。它对每个点云调用 RipsER 计算 H0、H1 两维的持久图并输出四个常见特征拓扑特征数量、平均寿命、最大寿命和总持久性。from ripser import Rips # 复用同一个 Rips 对象避免反复初始化 rips Rips(maxdim1, verboseFalse) def compute_topological_features(series, embed_dim10, embed_step2): 输入一段收益率序列 输出H0 和 H1 的拓扑特征字典 pc build_point_cloud(series, embed_dim, embed_step) # 对每个维度做标准化减弱量纲差异的影响 pc (pc - pc.mean(axis0)) / (pc.std(axis0) 1e-9) dgms rips.fit_transform(pc) feats {} for dim in range(2): dgm dgms[dim] # 去掉死亡时间为无穷大的点 finite dgm[np.isfinite(dgm[:, 1])] if len(finite) 0: lifetimes np.array([0.0]) else: lifetimes finite[:, 1] - finite[:, 0] feats[fh{dim}_num] len(finite) feats[fh{dim}_mean_life] lifetimes.mean() feats[fh{dim}_max_life] lifetimes.max() feats[fh{dim}_total_pers] lifetimes.sum() return feats这里有几个实现细节需要说明Rips(maxdim1)会同时计算 H0 和 H1H2 及以上维数计算成本更高在行情结构研究中通常先从 H1 开始验证。持久图中包含死亡时间为正无穷的点它们代表始终存在的连通分量或环统计寿命时通常剔除否则会出现无穷大值。对点云按列标准化后持久图的绝对尺度会发生变化但特征之间的相对大小仍可用于因子排序。如果你希望保留行情的原始振幅信息可以去掉标准化步骤。4.4 滚动计算因子有了拓扑特征提取函数之后就可以滚动生成因子时间序列。参数选择如下analysis_window 60每个因子样本回溯 60 个交易日。embed_dim 10子窗口长度。embed_step 2点云内部滑窗步长。sample_step 5每隔 5 个交易日计算一次因子降低计算量。代码如下analysis_window 60 embed_dim 10 embed_step 2 sample_step 5 records [] for end in range(analysis_window, len(df), sample_step): window_ret df[ret].iloc[end - analysis_window:end].values feats compute_topological_features(window_ret, embed_dim, embed_step) feats[date] df.index[end - 1] records.append(feats) factor_df pd.DataFrame(records).set_index(date) print(factor_df.head())这个循环的本质是在每一个因子生成日只使用当天之前analysis_window个交易日的数据计算拓扑特征不包含未来数据这符合因子计算中“避免前视偏差”的基本要求。输出会得到一个包含多列拓扑特征的因子表例如h0_numH0 连通分量数量通常与分析窗口内点云的点数接近。h1_numH1 环的数量。h1_total_persH1 总持久性是后续最常用的拓扑因子。h1_max_lifeH1 最大寿命代表最显著环的持续尺度。4.5 因子有效性初评IC 与分组收益因子构造完成后需要评估它是否与未来收益存在相关性。这里使用未来 20 日收益作为预测目标from scipy.stats import spearmanr # 计算未来 20 日收益 factor_df[ret_future20] ( df[close].shift(-20) / df[close] - 1 ).reindex(factor_df.index) # 全局 Spearman IC for col in [h0_total_pers, h1_total_pers, h1_max_life]: ic, pval spearmanr(factor_df[col], factor_df[ret_future20]) print(f{col}: Spearman IC {ic:.4f}, p-value {pval:.4f})Spearman IC 衡量因子值和未来收益之间的秩相关性取值范围在 -1 到 1 之间。绝对值越大说明因子的单调预测能力越强。通常在实际因子研究中我们会观察 IC 的均值、标准差和 IC 0 的占比而不能只看某一段时间的 IC。分组收益检验可以让因子的单调性看得更直观# 按 H1 总持久性分成 5 组 factor_df[quantile] pd.qcut( factor_df[h1_total_pers], 5, labelsFalse, duplicatesdrop ) group_ret factor_df.groupby(quantile)[ret_future20].mean() print(group_ret)如果因子的分组收益呈现明显的单调上升或下降说明该因子对收益截面有较强的解释力如果分组收益没有规律则说明该拓扑特征在模拟数据上没有预测价值。4.6 结果解读需要强调使用随机模拟数据运行上述脚本时IC 通常会接近 0分组收益也不会有明显单调性。这是正常现象因为几何布朗运动产生的价格序列本身没有可预测的结构。更重要的是这套流程可以完整地运行并且已经展示了从行情序列到拓扑因子再到因子检验的全部环节。当你把数据源换成真实行情数据后需要注意两个判断维度第一拓扑特征是否稳定。如果某个标的的 H1 特征时有时无说明它的行情结构在不同时期差异较大因子适用性有限。第二拓扑因子是否提供增量信息。即使拓扑因子本身 IC 较高也需要和传统动量、波动率因子做正交化检验确认它不是已有因子的重复表达。5. 常见问题与排查思路5.1 计算量太大、内存暴涨Rips 持久同调需要计算点云两两之间的距离因此复杂度随点数增加明显上升。如果点云数量超过几千个点可能会遇到内存不足或计算时间过长的问题。可以采取以下优化手段增大embed_step减少点云内的点数。增大sample_step减少滚动因子的频次。对点云进行随机降采样但要注意不要破坏核心结构。分时段计算避免一次性处理整段超长历史数据。在实际项目中更推荐把持久图计算缓存下来。因为同样的行情数据在参数不变时拓扑特征不会变化重复计算只会浪费计算资源。5.2 持久图里大量噪声点持久图中的点数量很多并不代表行情结构丰富。真正的“有效结构”通常只占少数大多数点离对角线非常近可以被视为噪声。处理思路是设置寿命阈值# 例如只保留寿命大于 0.1 的特征点 valid dgm[np.isfinite(dgm[:, 1])] valid valid[valid[:, 1] - valid[:, 0] 0.1]但寿命阈值的设定需要谨慎过大的阈值会丢失真实结构过小的阈值又无法过滤噪声。建议先画出持久图观察分布再决定阈值范围。另外数据标准化会显著影响寿命大小。如果对点云做了标准化特征的寿命阈值需要重新校准不能直接使用未标准化时的经验值。5.3 H1 特征不稳定H1 特征常常出现时有时无的情况尤其在行情趋势非常强的时候收益率点云可能呈一条倾斜的流形内部没有明显的环状结构因此 H1 总持久性可能为 0。面对这种情况可以考虑使用 H0 和 H1 的组合特征而不是只依赖 H1。在更长周期上观察例如用周线数据代替日线。引入成交量、振幅等多维特征让点云包含更多结构信息而不是只使用收益率。5.4 参数选择与过拟合拓扑因子涉及的参数较多例如analysis_window、embed_dim、embed_step和sample_step。参数不同因子分布可能差异很大。如果在某组参数下 IC 很高但参数稍微调整就失效很可能是在过拟合历史数据。推荐的做法是做一个参数敏感性矩阵固定其他参数对目标参数从小到大变化观察 IC 的均值和稳定性。如果因子在较宽的参数范围内都表现稳定那么它进入组合模型的可信度会更高。常见问题汇总如下问题现象常见原因解决思路安装 ripser 失败pip 版本过旧、缺少编译器升级 pip、安装构建工具、使用虚拟环境计算时间过长点云点数过多增大采样步长、降采样、缓存结果持久图全是噪声数据未标准化或窗口过短调整嵌入维度、设置寿命阈值H1 特征经常为 0行情本身缺乏稳定环状结构使用 H0/H1 组合特征、改成周线数据IC 波动剧烈参数过拟合参数敏感性分析、样本外测试因子与传统因子高度相关拓扑特征和已知统计量重复做因子正交化计算增量 IC6. 工程实践与量化应用建议6.1 数据清洗与预处理拓扑特征对输入数据的量纲和尺度比较敏感。如果点云中同时包含收益率、成交量和振幅不同特征的数值范围差异很大距离度量会被数值较大的维度主导。建议在构造点云前统一做标准化或者先对各列做 winsorize 处理降低极端值的影响。同时要注意行情数据中的除权除息、停牌、异常大单都可能产生孤立点或异常距离这会直接污染持久图。最好先做复权处理和停牌过滤再进入 TDA 计算流程。6.2 因子去极值与中性化计算出的拓扑因子往往带有明显的截面分布偏度直接参与回归或排序可能受到极端值影响。在进入多因子模型前建议做以下处理去极值例如用中位数加减 5 倍 MAD 截断。标准化将因子转换为 z-score。中性化如果因子与市值、行业、波动率大小存在显著相关可以用线性回归把因子残差化得到风险中性后的拓扑因子。这一步对所有因子都适用拓扑因子也不例外。否则后续的 IC 分析可能只是因为因子间接暴露在行业或市值因子上。6.3 滚动窗口与计算缓存在真实项目中通常需要每天或每周更新因子而不是只计算一次。拓扑特征的计算成本比普通统计特征高建议把持久图结果按“标的 参数 截止日期”缓存