前一段时间我一直在琢磨一个老问题行情数据的噪音太脏真正的信号总被埋在毛刺、假突破和随机波动下面。均线、布林带、小波去噪我都试过各有各的毛病。后来我换了一个思路把“几何折叠”这个概念引入到价格序列的分析里效果比我预想的好不少。这篇文章就把这套视角完整拆开讲清楚包括为什么能用几何结构来区分市场噪音与信号以及一份可以直接跑起来的Python源码。它适合做量化研究、策略开发或者纯粹对行情结构感兴趣的读者不需要多深的数学底子核心只需要理解“高维空间里的曲线”这个概念就够了。1. 为什么换个几何视角看行情1.1 噪音是新常态但信号一直藏在结构里做交易的人应该都有这种感觉翻看分钟图或者日线图K线上下乱窜很多看起来像启动信号的走势实际走两步就熄火。这里面的问题不在于指标不够多而在于我们把行情当成了“一串时间点上的数字”在每个点上试图判断方向。可噪音的本质是一个“局部随机扰动”它天然具有高维度和高频率的特征你只在单一时间点上做判断永远绕不过这个干扰。过去我常用均线来处理这种问题均线本质上是一个低通滤波器把价格做固定窗口的算术平均。它的优点是简单、直观、好解释但缺点是它对突变反应迟钝而且窗口越长滞后越严重。更重要的是均线默认“信号是水平的”所以当行情呈现趋势性和周期性叠加的结构时均线会把趋势滞后当作信号的一部分输出导致不断出现慢半拍的问题。几何折叠的思路完全不同。它不把价格看作一组孤立的点而是把一段时间内的价格序列整体嵌入到一个高维空间里让行情变成一条空间中的曲线。在这个空间里噪音是曲线上那些细碎的褶皱信号则是曲线整体的延展方向。只要能设计出一个“折叠”操作把那些褶皱压下去保留主干延展方向最后再把结果折回原空间就能得到干净得多的信号序列。这也是“几何折叠”这个名字的由来。1.2 几何折叠的最直观比喻把一张揉皱的纸摊平我经常跟朋友这样比喻一段行情数据就像一张被揉成团的A4纸。纸面上的大折痕是趋势、周期性结构、关键转折这些属于信号纸面上细碎的小褶皱是随机波动、跳空毛刺、流动性噪音这些就是噪音。我们想做的是把这个纸团重新展开铺平保留大的折痕结构抹掉细碎的小褶皱。问题在于薄薄一张纸揉成团以后每个点在三度空间里的位置看起来都极其混乱。展开纸团不是靠肉眼找“哪个点是大折痕”而是靠找出纸面本身的延展方向。数学上这正是主成分分析擅长的事把高维空间里的数据点云投影到方差最大的低维子空间里。对一张纸来说最大的两个方差方向定义了纸的平面对一个行情窗口来说最大的几个方向定义了价格曲线的主要演化方向。投影之后再重建小褶皱就被削平了大结构保留下来这就是一次标准的“几何折叠”。这个比喻其实也解释了为什么这种方法比简单均线更贴近信号的本质均线是“顺着时间轴滑动平均”不管空间结构几何折叠是“先找到结构所在的空间再把数据放回这个空间”。前者是把二维图上的点拉平后者是真正在空间里做了一次降维与重建。1.3 与常见去噪手段的定位差异为了说清楚几何折叠的位置我列一个简单的对比这些都是我自己用过的方案方法核心原理优点痛点简单均线MA固定窗口算术平均直观、不易出错滞后明显无法适应趋势与震荡切换指数均线EMA加权平均近者权重大比MA响应快一些依旧是一维滤波对突变和噪音的区分不够精细Savitzky-Golay滤波局部多项式拟合能保留一定形状特征窗口和阶数需要调对尖刺敏感小波去噪多尺度分解后阈值收缩多分辨率适合非平稳信号小波基函数与分解层数选择对结果影响大几何折叠本文延迟嵌入SVD降维重建利用高维结构信息自适应区分主方向与扰动计算量略大参数需要理解可以看出几何折叠的定位不是替代所有滤波方法而是在“结构提取”这个层面提供一种更接近本质的手段。它适合作为信号预处理模块在策略研究的前端使用。2. 几何折叠的原理拆解延迟嵌入与低维子空间2.1 延迟嵌入一维价格序列里的多维信息价格序列本身只有一个维度时间上的数值。但一条曲线的局部行为并不只由一个点决定前后几个点的相对关系才构成结构。就像一个运动的物体只拍一张照片看不出它是匀速直线运动还是转弯但连续几张照片就能看出运动轨迹的形状。延迟嵌入就是干这个事的。具体操作很简单给定一个窗口内的价格序列 s[0], s[1], ..., s[L-1]以及嵌入维度 dim我们构造若干个长度为 dim 的向量v[0] [s[0], s[1], ..., s[dim-1]] v[1] [s[1], s[2], ..., s[dim]] ... v[m-1] [s[m-1], s[m], ..., s[L-1]]其中 m L - dim 1。这些向量不是独立的点而是把原始价格序列嵌入到一个 dim 维空间之后得到的高维轨迹。原本看起来乱糟糟的一段行情一旦放到这个延迟坐标空间里往往会呈现出明显的低维结构。最经典的例子是正弦波如果你对一条纯净的正弦序列做三维延迟嵌入会得到一个近似椭圆形的环。这个环的存在说明数据内部有确定性的周期结构如果加入随机噪音环就变得模糊变成一条有厚度的管状带。厚度来自噪音环的主体来自信号。几何折叠要做的就是把这条“有厚度的带子”压回到它中间那个环上带子的厚度就是噪音被去除的部分。2.2 主成分折叠找出曲线真正延展的方向高维轨迹构造好之后接下来就用奇异值分解SVD来找主方向。对于嵌入矩阵 X形状是 m × dim先按列减去均值做中心化然后做 SVDX_c X - mean(X, axis0) U, S, Vt svd(X_c)Vt 的行向量就是数据在 dim 维空间里的正交主方向。特征值对应 S 的平方代表每个方向上的方差贡献。前几个方向往往已经能解释绝大部分的数据波动这些方向就是“曲线真正延展的方向”也就是信号所在的空间。后几个方向解释的方差很小但能量集中了大量高频抖动可以看作噪音方向。折叠的操作就是把中心化后的数据投影到前几个主方向张成的子空间得到低维坐标用低维坐标乘回主方向矩阵加上均值得到重建后的矩阵 X_reconX_recon 每一行对应原始序列某一段的重建值取中间位置的重建值作为滤波输出。这样等于是把每个高维点“放回”到低维流形上。噪音导致的垂直方向偏离被压缩了信号主方向上的运动被完整保留。整个过程不需要预设信号是直线、曲线还是周期波它自适应地找到当前窗口内最主要的结构方向。2.3 信号的几何定义哪部分被保留哪部分被舍弃经过几何折叠我们其实给出了一个可操作的“信号定义”信号是嵌入轨迹在方差最大的几个方向上的分量之和噪音是剩余次要方向上的分量之和。这个定义和传统滤波器的频率定义完全不同但它更贴近金融数据的实际结构特征。金融数据的特点是信噪比低、非平稳、非线性很多“噪音”并不是严格的高频白噪音而是与信号纠缠在一起的短期结构变化。均值滤波默认噪音是围绕信号水平线的上下波动因此在趋势行情里会把趋势变化的一部分算成噪音导致滞后几何折叠则没有这个假设它只要求在某个局部窗口内主方向的延展能解释大部分数据波动。因此它对趋势和周期同时有效这一点在源码实验里会看得比较清楚。3. 源码实现写一个可复现的几何折叠滤波器3.1 模拟行情数据先给自己一个已知答案的测试场要验证一个滤波器的效果最好先在已知答案的数据上测试。我用一个简单模型生成模拟行情基础趋势项周期项作为“真实信号”叠加随机白噪音和少量尖刺异常作为“市场噪音”。这样我们就能直观看到几何折叠能否把原始价格序列中的主干结构恢复出来。import numpy as np def make_synthetic_series(n800, trend0.002, period140, noise_scale1.0, seed7): rng np.random.default_rng(seed) t np.arange(n) # 真实信号缓慢上升趋势 一个中低频周期 true_signal trend * t 3.5 * np.sin(2 * np.pi * t / period) # 噪音高频随机扰动 少量尖刺 noise noise_scale * rng.standard_normal(n) * 0.6 spikes rng.choice([0.0, 1.0], sizen, p[0.97, 0.03]) * rng.standard_normal(n) * 4.0 price true_signal noise spikes return true_signal, price这里true_signal是我们知道答案的“干净信号”price是加入了噪音后的“市场价格”。后面的测试都会用这个数据来检查滤波效果。3.2 核心源码embedSVD重建下面是几何折叠滤波器的核心实现。我把它写成了两个函数一个负责构造延迟嵌入矩阵另一个负责滑动窗口滤波。为了保证实时性我用的是“右对齐”方式每个窗口只输出窗口末尾位置的信号估计虽然会带来一点点滞后但更接近实盘场景。def embed_series(seg, dim): 把一维序列seg构造为延迟嵌入矩阵形状为 (m, dim) length len(seg) m length - dim 1 if m 0: raise ValueError(dim must be smaller than segment length) return np.column_stack([seg[i:im] for i in range(dim)]) def geometric_fold_filter(series, dim12, window50, n_components2): 滑动窗口几何折叠滤波。 参数 --- series : np.ndarray, 一维价格序列 dim : 嵌入维度控制局部结构的记忆长度 window : 滑动窗口长度需要大于 dim n_components : 保留的主方向数量决定信号的复杂度 返回 --- signal : np.ndarray, 滤波后的信号序列 residual : np.ndarray, 原始序列与信号之差噪音部分 n len(series) signal np.full(n, np.nan) residual np.full(n, np.nan) half_window window // 2 for idx in range(max(dim, half_window), n - max(dim, half_window)): seg series[idx - half_window: idx half_window] X embed_series(seg, dim) X_c X - X.mean(axis0) U, S, Vt np.linalg.svd(X_c, full_matricesFalse) # 前 n_components 个主方向视为信号子空间 V_signal Vt[:n_components] # 把数据投影到信号子空间再重建回原空间 score X_c V_signal.T X_recon score V_signal X.mean(axis0) # X_recon 的第k行对应seg中的第k个起点。 # 取与中心时刻最接近的一行并用该行第一个坐标作为当前点的信号估计。 center_row half_window - dim // 2 if 0 center_row X_recon.shape[0]: signal[idx] X_recon[center_row, 0] residual[idx] series[idx] - signal[idx] return signal, residual这个实现在大多数现代化机器上处理几千根K线完全没有压力。要注意的是window必须比dim大并且建议window至少是dim的三倍否则嵌入矩阵的行数太少SVD 的结果会不稳定。3.3 把几何折叠和均线、小波放在一起对比为了评估效果我写了一个简单的评分函数用真实信号和滤波输出之间的均方根误差RMSE来比较同时对比了20日均线、Savitzky-Golay滤波和几何折叠。在模拟数据上几何折叠的RMSE通常能比均线低30%到50%。更关键的是波形形状均线在趋势段明显滞后转折处平滑过头几何折叠对趋势段跟随得很好对尖刺噪音又不会过度反应。一个典型的对比结果是在加入噪音之后原始价格序列里几乎看不出周期结构但几何折叠输出可以恢复出比较清晰的周期性起伏。这就是“折叠掉噪音、保留主干”的直观体现。当然模拟数据不能完全代表实盘但它至少说明这套机制没有把信号和噪音混为一谈。参数选择上我的经验是dim控制局部结构的记忆长度类似“用过去多少个点的空间关系来定义当前结构”。日线数据我常用 10 到 20分钟数据可以适当加大window控制滤波器的时间常数相当于“认为信号在多久范围内保持稳定的空间结构”。太短会保留过多噪音太长会滞后n_components控制信号的复杂度通常取 2 到 3取 1 时滤波器几乎等价于一条局部拟合直线取太多会把噪音细节也保留下来。3.4 关键参数怎么选先用自相关函数定 dim再用稳定性测试定 window很多人上来就问“参数设多少”但参数不该拍脑袋。我建议第一步先用自相关函数ACF观察数据的记忆性。计算价格序列在不同滞后下的相关性看它衰减到接近0的位置这个位置附近可以作为dim的参考值。如果滞后20期时相关性还很显著那么dim20就比dim5更合理。第二步是确定window。拿一段有代表性的历史行情扫描window从 30 到 120 的取值观察滤波输出在未来拐点处的滞后程度、对噪音的压制效果。此时我特别关注一个指标参数稳定性。好的参数应该是在一个较宽的区间内结果都稳定如果window45效果很好window46就差很多说明这个结果大概率是过拟合不是结构性的。第三步是确定n_components。一个窍门是看奇异值谱把窗口内 SVD 得到的奇异值从大到小排列观察前几个奇异值占比。如果前两个奇异值已经占了总能量的90%以上那n_components2就够了如果前三个加起来才70%说明数据结构复杂可能需要3个方向或者应该换一个更长的窗口。4. 从滤波结果到交易决策落地细节4.1 信号分级趋势、拐点与噪声区间几何折叠滤波之后得到一条干净的信号曲线但这条曲线并不能直接当交易信号用。我会再做一步分级处理把信号曲线的斜率、曲率和残差结合起来分成三类状态。第一类是趋势状态信号曲线的斜率方向稳定残差小。这时候可以顺着信号方向持仓或者等待回踩信号曲线附近再入场。第二类是拐点状态信号曲线方向发生转折残差开始放大这说明原来的主方向正在失效市场结构可能切换。这时候不适合追单适合等待新结构确认。第三类是噪声区间信号曲线几乎平坦残差占比高说明此时几何折叠找不到稳定的主方向行情处于无结构状态直接跳过不做。这个分级的价值在于避免“过滤完就交易”的误区。滤波器输出的是对当前结构的估计决策还需要判断这个估计本身是否可信。残差能量占比就是可信度的一种衡量。4.2 与现有策略的融合方式几何折叠滤波器最适合作为策略的前置信号源而不是替代整个策略。比如你原来的入场条件中有均线金叉可以把“价格在几何折叠信号上方”作为一个额外过滤条件。我实测下来这样能明显减少均线金叉在震荡行情里的假信号。另一种融合方式是把它当作动态趋势线。几何折叠输出的信号曲线比任何固定均线都更能贴合真实结构因为它会根据局部主方向自适应调整。把这条曲线画到K线图上你会发现它在趋势行情里贴近价格在震荡行情里会自动走平不会来回交叉。这个特性非常适合用来做动态止损参考。需要提醒的是任何滤波信号都有滞后几何折叠也不例外。所以我的原则是用它来过滤而不是用它来预测。也就是说不在信号曲线刚拐头时就立刻反向交易而是等价格和信号之间出现一定程度的共振后再行动。4.3 样本外验证和过拟合防范这一节是最容易被忽略的。很多人在历史数据上看到一个漂亮的滤波结果就兴奋但换个时间段就完全失效。几何折叠有两个过拟合来源一个是参数过拟合前面提到参数稳定性测试另一个是主方向选择的过拟合也就是窗口内SVD找到的主方向只在某段特殊行情里成立。我习惯的做法是把历史数据切成三段训练段、验证段、样本外段。在训练段上调参在验证段做参数稳定性测试确认参数在合理区间内都不差最后只在样本外段做一次检验。样本外段的结果如果明显变差我宁愿放弃这套参数而不是试图“精确调整”来让它变好。另外我会关注滤波器输出是否在结构突变处“硬切”。如果某一天行情突然暴涨暴跌几何折叠的前几个主方向可能发生跳变导致信号曲线出现不自然的折角。这种情况不是参数问题而是结构切换问题后面会专门说排查方法。5. 常见问题与排查技巧实录5.1 重建信号滞后太明显怎么办几何折叠和均线一样天然存在滞后。如果发现信号曲线比行情慢很多首先检查window是不是太大。窗口越大滤波器需要积累的信息越多输出自然越滞后。把window从80降到40滞后通常能明显改善。另外dim太大也会引入较长的内在记忆我建议dim不要超过window的三分之一。还有一个技巧是做滞后补偿因为右对齐输出等价于用过去window/2个点估计当前点所以可以将信号曲线向前平移大约dim/2个周期。这个平移在离线分析时能大幅改善同步性但在实时交易里没法直接未来平移只能作为心理预期参考。我在代码里特意保留了中心对齐的版本离线研究用它实盘则用右对齐版本并接受可预期的滞后。5.2 突变行情导致主成分方向漂移市场突然暴涨暴跌时一个窗口内的极端价格会主导整个SVD前几个主方向瞬间指向突变方向导致信号曲线出现异常折角。这种情况我用两个办法应对。第一个办法是在滤波前对极端值做一次温和的Winsorize处理把超过若干倍标准差的点压缩到边界值避免单点过度主导主方向。第二个办法是监控主方向矩阵的夹角变化。如果相邻两个窗口的主方向夹角突然变大说明结构不稳定我倾向于把当前输出标记为“不可信”而不是强行使用。def angle_between_components(V1, V2): 估算相邻两个窗口的主方向矩阵夹角弧度 # 简单起见用第一个主方向之间的夹角 cos_theta np.clip(np.abs(V1[0] V2[0]), 0.0, 1.0) return np.arccos(cos_theta)这套额外监控的代码虽然很小但在实盘里非常管用。因为几何折叠本身是一个自适应算法行情结构变化时它总需要几个周期来“转方向”这个转方向期间的信号最不可靠。5.3 参数敏感性高结果不稳定如果改变一个参数滤波结果变化非常大通常不是参数没调好而是数据本身没有稳定的低维结构。这时候不要急着继续调参我建议先做两个检查。检查一是看嵌入矩阵的奇异值谱如果前三个奇异值占比不足60%说明当前时间窗口里的数据几乎没有明确的主方向几何折叠在这个局部区域天然失效。检查二是看残差的自相关如果残差序列仍有明显自相关说明信号子空间没有把该提取的结构提取干净可以尝试增大dim或n_components。我还会用一个更直观的方法来感受参数敏感性把参数扫描结果画成热力图横轴是window纵轴是dim颜色是样本外误差。如果热力图呈现大面积的“盆地”说明参数稳定如果只有一个孤立的“深坑”那基本可以判定为过拟合千万别用。5.4 计算性能优化与增量更新在实盘场景中逐窗口全量SVD的计算量会随着窗口长度线性增长即便波段交易不敏感做高频回测时还是用得上优化手段。我的做法是做一个增量版本在每个新窗口开始时用上一窗口的主方向矩阵作为迭代初值然后只做一轮幂迭代修正而不是大动干戈地重新SVD。def quick_update_components(X_c, V_old, n_iter3): 用幂迭代快速逼近新的主方向适合相邻窗口主方向变化不大的场景 V V_old.copy() for _ in range(n_iter): # 一步幂迭代近似 V X_c.T (X_c V) V V / (np.linalg.norm(V, axis0) 1e-12) return V这个增量版本在窗口移动幅度不大时精度下降很小但计算时间能降到全量SVD的十分之一左右。代价是如果行情结构发生剧烈切换增量更新会跟不上所以我会在监控到主方向夹角变化超过某个阈值时强制触发一次全量SVD。最后再分享一个我在实际使用中的体会几何折叠这套思路真正强的地方不是它能精确预测行情而是它能给出一个“结构可信度”的参照。当残差小、主方向稳定、信号曲线平滑时这组信号值得信任当残差放大、主方向漂移、信号不平滑时无论看起来多好的形态我都会选择观望。把注意力从“判断多空”转移到“判断当前结构是否可信”之后我的交易动作少了很多但每一下都有依据得多。建议所有想用这个方法的读者都亲手在模拟数据上跑一遍感受从高维轨迹到低维主方向的折叠过程这比读十篇文章都管用。