用FFT傅立叶变换预测股价:频谱分析、回测与避坑实战 做量化分析的人多多少少都想过一件事股价到底能不能被预测我自己在这个方向上折腾了不少时间试过经典的均线策略也试过用LSTM做序列建模后来兜兜转转反而被一个老工具吸引了——傅立叶变换(FFT)。这东西在信号处理里几乎是“万金油”把一段波拆成不同频率的正弦波组合用到股价序列上就是去识别行情里隐藏的周期成分。这篇文章想跟你聊聊我是怎么用FFT做股价预测的包括完整的数据处理流程、频谱分析的实现细节、回测结果以及我自己踩过的坑。适合对量化交易有基础了解、想尝试信号处理思路的朋友参考。我不会说“靠FFT就能稳赚”恰恰相反这篇文章会花不少篇幅讲它为什么有天花板——但即便如此理解FFT的思路对你做趋势判断、周期识别仍然非常有价值。1. 为什么我拿FFT去做股价预测1.1 一开始的动机和踩过的坑最早我是在研究“周期选股”这个概念。A股和美股市场里很多标的确实存在季节效应、日历效应比如某些月份收益明显偏高某些行业在特定宏观周期里表现更强。传统的做法是用自相关函数去识别周期或者直接看K线图数高低点。但人工数K线的问题在于行情里同时叠加了多个周期——日内波动、周内节奏、月度趋势、季度调整全都混在一起肉眼很难拆开。后来我接触到频谱分析才意识到这其实是“多周期叠加信号分解”问题。FFT能在一个序列里找出所有周期成分的强度和相位相当于把混合在一起的信号分离开。这个思路一下子就戳中了我如果我能知道当前标的主要由哪些周期构成是不是就能顺着这些周期的延续性做外推预测当时我拿某只宽基指数ETF的日线数据做了第一版实验跑出来的结果让人兴奋回测曲线看起来非常漂亮。但后来冷静下来一查发现有两处严重的逻辑漏洞一是用了全量数据做FFT再预测同段区间属于未来函数二是没有做滑窗导致极端的端点效应。这篇文里我讲的版本是修复了这两个问题之后的改进流程你直接照做不会踩同样的坑。1.2 FFT到底在算什么写给没学过信号处理的人很多做交易的朋友一听“傅立叶变换”就觉得是数学天书其实它背后的直觉很简单。任何一条看似毫无规律的曲线都可以被拆成若干条不同频率、不同幅度、不同起点的正弦波叠加。低频正弦波对应“慢节奏的大趋势”高频正弦波对应“快节奏的小波动”FFT就是帮你把“拆开后每一部分的频率、幅度、相位”都算出来。举个例子。你把一首歌的人声和伴奏混在一起FFT可以把混合信号拆开单独看某段频率范围的能量。股价序列也是同样的道理日K线的最高价、最低价、收盘价混合了多种时间尺度的市场行为FFT做的就是“把行情混音轨拆分”的工作。拆完之后你挑出能量最大的几条“主音轨”忽略那些相当于噪音的微弱频率再用这些主音轨重新合成未来的走势这就是FFT预测的核心逻辑。注意这里说的“重新合成未来的走势”本质上是假设“过去的主频成分在未来一段时间内仍然延续”。这个假设在刻画趋势时有参考意义但市场不是匀速的振动系统它随时可能改变频率结构。1.3 FFT能预测股价吗——先泼一盆冷水先说结论FFT不适合作为独立的择时信号但它非常适合做“趋势过滤”和“周期定位”。换句话说它不能告诉你明天涨还是跌但能帮你判断当前处于什么级别的周期阶段。原因有三个。第一FFT假设信号是平稳的而股价收益率序列通常是非平稳的直接对价格做FFT会得到一堆虚假的强频率。第二市场的频率成分不是固定的一个周期可能在某个阶段起主导过一段时间就消失FFT没有自适应能力。第三端点效应很严重序列最后几个点的重建误差会非常大而预测恰恰就是从最后几个点开始的。所以这套方案的定位很明确把FFT当成一种特征提取器或者一个“辅助预测器”和动量指标、均线系统、基本面信号组合使用而不是单独靠它下注。理解了这层定位后面的细节才有意义。2. 数据准备把K线变成干净的序列2.1 数据源与标的选择我这次实验用的是某只流动性较好的行业ETF的日线收盘价模拟了2021年1月到2025年6月约1000多个交易日的数据。选取ETF而不是个股是因为ETF的噪声相对小不容易被单日极端涨跌带偏适合验证周期思路。拿到原始数据后先做常规清洗剔除停牌日、处理除权除息造成的价格跳空用前复权价格。很多初学者会忽略复权这一步直接拿原始价格算FFT遇到大比例分红会让频谱里多出一个虚假“低频大周期”这个坑我踩过一次特别提醒你注意。数据长度上我用的是120个交易日作为FFT窗口。为什么是120而不是更长因为1000多个交易日里涵盖了几轮不同级别的行情如果窗口太长序列里叠加的行情阶段太多频谱会被摊平主频不突出如果窗口太短频率分辨率太低最低能识别的周期大概只有窗口长度的一半120天窗口大约能识别60天以上的周期已经可以覆盖一个季度的节奏。2.2 去趋势与平稳化处理这是整个流程中最关键的一步。直接对原始收盘价数组做FFT低频部分会占据绝对主导高频细节全被淹没本质上你只是在拟合一条“弯曲的趋势线”而不是在分析周期波动。所以必须先去掉趋势项。我的做法是两步走import numpy as np def detrend(close, methodlinear): x np.arange(len(close)) if method linear: coef np.polyfit(x, close, 1) trend np.polyval(coef, x) else: # 更平滑的备选方案简单移动平均去除短期波动 trend pd.Series(close).rolling(window20, centerTrue).mean().bfill().ffill() return close - trend, trend建议先用线性回归去除全局趋势然后对剩余序列再做一次FFT。如果你发现去趋势后的序列仍有明显波动不均匀可以用对数价格代替原始价格因为对数价格更接近百分比变化消除不同价位区间的方差差异。提示不要对原始价格直接用FFT也不要用差分代替去趋势。差分后的序列虽然平稳了但会丢失低频信号同样不利于周期识别。去趋势是保留低频周期结构的同时剔除趋势漂移差分则把低频信号直接杀掉了。2.3 滑窗切片与标签构建为了避免未来函数我采用滚动窗口的方式构建样本。假设FFT窗口长度为L120预测步长为H5那么在时间点t只用已有数据[t-L1, t]做FFT预测[t1, tH]区间的走势然后窗口整体向后滚动重复执行。这里有两个细节需要说明。第一滑窗步长不一定等于预测步长我习惯用步长等于预测步长即每5个交易日做一次预测避免样本之间的高度重叠导致回测结果虚高。第二预测的“标签”不是具体涨跌数值而是未来H日的累计收益率方向和幅度这样便于后续统计方向准确率。数据切分上我用前60%做参数调试后40%做最终验证。参数调试部分要避免反复看结果调参否则会有过拟合风险这个后面展开讲。3. 核心实现FFT频谱分析与预测重构3.1 计算频谱幅度谱与相位谱对去趋势后的序列做离散傅立叶变换核心代码非常短from scipy.fft import fft def compute_spectrum(signal): n len(signal) spectrum fft(signal) freqs np.fft.fftfreq(n, d1) amp np.abs(spectrum) / n phase np.angle(spectrum) return freqs, amp, phase, spectrum这里要理解几个关键概念。幅度谱的横坐标是频率表示“每多少个交易日重复一次”比如频率为0.01对应100个交易日一个周期。纵坐标是幅度表示这个周期成分在原始信号中的能量占比。相位谱则描述了每个周期当前处于波峰还是波谷、以及偏移多少。用这个函数计算某段行情时你会看到在低频区有几个明显的尖峰这些尖峰对应的就是当前行情的主导周期。实际行情中常见的主周期包括20至30个交易日的月度节奏、60至90个交易日的季度节奏以及更长的半年到一年的大周期。高频区则是一片杂乱的低幅度谱线这就是噪声。3.2 主频筛选保留多少分量保留多少频率分量直接决定预测结果的平滑度和拟合度。保留太多会把噪声也当成周期信号预测结果跟着噪声剧烈摆动保留太少会丢失重要结构预测结果过于平滑几乎接近一条直线。我的做法是设定“能量占比”阈值而不是固定保留数量。计算所有频率分量的能量幅度平方和然后从最大分量开始累加直到累积能量达到总能量的85%或90%取此时的分量集合作为主频集合。经验上120天窗口下通常只需要保留5到15个分量就能达到85%的能量占比。def select_components(amp, energy_threshold0.85): energy amp ** 2 total energy.sum() idx_sorted np.argsort(energy)[::-1] cum np.cumsum(energy[idx_sorted]) / total keep idx_sorted[cum energy_threshold] # 确保至少保留一个分量 if len(keep) 0: keep idx_sorted[:1] return keep为什么用能量阈值而不是固定数量因为不同行情下的频谱结构差异很大。震荡行情下能量分散需要更多分量才能达到85%的阈值趋势行情下能量集中少数几个低频分量就足够。固定数量会在震荡行情里混入噪声、在趋势行情里丢失细节而能量阈值能做到自适应。3.3 用逆变换重构未来点这一步是FFT预测的核心也是争议最大的一步。基本思路是把筛选后的主频分量保留把噪声分量置零然后在频率域末尾“补零”再做逆傅立叶变换得到的历史段重建序列就是去噪后的信号而补零段对应的时域数据就是这些主频自然延续出来的“预测”。from scipy.fft import ifft def fft_forecast(signal, keep_idx, future_steps5, energy_threshold0.85): n len(signal) spectrum fft(signal) amp np.abs(spectrum) keep select_components(amp, energy_threshold) filtered np.zeros_like(spectrum) filtered[keep] spectrum[keep] extended np.concatenate([filtered, np.zeros(future_steps, dtypecomplex)]) recon ifft(extended).real history_recon recon[:n] future_recon recon[n:] return history_recon, future_recon这段代码背后有一个数学上不那么严谨、但实践中常用的假设把频谱“延伸”到更高的频率分辨率位置相当于认为当前主频分量的相位和幅度在未来继续旋转。本质上就是在做“用基波频率外推信号”和你用正弦函数拟合历史数据后把曲线往后画是一个道理。更严谨的做法是不用补零而是直接用每个主频分量的频率和相位写出解析表达式然后计算未来时刻的值def forecast_by_sines(t_history, t_future, freqs, amp, phase, keep_idx): future np.zeros(len(t_future)) for idx in keep_idx: if idx 0: continue f freqs[idx] a amp[idx] p phase[idx] # 还原实际幅度fft归一化后的 a a * len(t_history) future a * np.cos(2 * np.pi * f * t_future p) return future / len(t_history)建议你优先用解析表达式的方式实现因为补零方式对非整数周期的信号会产生额外的边界失真而解析方式更可控。补零法代码简单适合快速验证解析法适合正经回测。3.4 代码逐段拆解给你一个完整的、可直接运行的预测流程框架import numpy as np import pandas as pd from scipy.fft import fft def fft_predict(close, lookback120, future_steps5, energy_threshold0.85): seq close.iloc[-lookback:].values detrended, trend detrend(seq, methodlinear) freqs np.fft.fftfreq(len(detrended), d1) spectrum fft(detrended) amp np.abs(spectrum) phase np.angle(spectrum) keep select_components(amp, energy_threshold) # 趋势部分外推线性趋势继续延伸 x_future np.arange(len(detrended), len(detrended) future_steps) trend_future np.polyval(np.polyfit(np.arange(len(detrended)), detrended, 1), x_future) # 上面的detrended已经是残差需要再把原始趋势加回来 original_trend trend # 原序列的线性趋势 trend_future np.polyval(np.polyfit(np.arange(len(seq)), seq, 1), x_future) t_history np.arange(len(detrended)) t_future np.arange(len(detrended), len(detrended) future_steps) future_detrended forecast_by_sines(t_history, t_future, freqs, amp, phase, keep) # 叠加趋势 future_price future_detrended trend_future return future_price注意我这里的写法是把原始序列本身的线性趋势外推再加上周期部分的外推两部分加起来得到最终的预测价格。实际测试下来如果只做“去趋势后FFT预测”然后再加回原趋势往往会高估趋势的持续性因为市场趋势本身也会反转。所以我在后续实验里更倾向于只预测“残差的方向”而不是直接预测具体价格。4. 回测设计与结果评估4.1 滚动回测框架有了预测函数接下来要放在历史数据里检验效果。回测框架长这样def backtest(close, lookback120, future_steps5, energy_threshold0.85, step5): preds [] actuals [] dates [] for end in range(lookback, len(close) - future_steps, step): hist close.iloc[end - lookback:end] future_price fft_predict(hist, lookback, future_steps, energy_threshold) pred_return future_price[-1] / hist.iloc[-1] - 1 actual_return close.iloc[end future_steps] / close.iloc[end - 1] - 1 preds.append(pred_return) actuals.append(actual_return) dates.append(close.index[end]) result pd.DataFrame({pred: preds, actual: actuals}, indexpd.to_datetime(dates)) return result这个框架的核心原则每个预测点只使用end之前的数据预测未来future_steps天后相对当前位置的收益率然后滚动步长为5天。这样既保证没有未来函数也减少了样本重叠。第一次跑这个回测我的方向准确率只有51%左右看着和抛硬币差不多。但注意这是FFT单独的表现。当我把它作为过滤条件叠加到趋势跟踪策略上时情况发生了变化。4.2 评价指标别被“预测准”骗了评价预测模型我最看重三个指标你回测时也建议重点看这几个指标含义参考意义方向准确率预测涨跌方向与实际方向一致的比例最直观但接近50%不代表没价值平均绝对误差(MAE)预测收益率与实际收益率的绝对差均值反映数值拟合精度信息比率(IC)预测值与实际值的相关系数反映预测排序能力选股场景重要FFT预测的MAE通常在2%到5%之间取决于标的是ETF还是个股但方向准确率很难稳定超过55%。更强的用法是结合预测方向与信号强度比如预测上涨幅度超过某个阈值时才开仓低于阈值时空仓这样能有效过滤掉低质量的预测。4.3 结果对比与改良方向我拿“FFT全量预测”和“去趋势FFT预测”做了对比。全量预测版本在趋势行情里表现尚可但在震荡行情里频繁给出假信号。去趋势版本整体更稳健预测曲线更好地贴合了实际波动节奏。后来我在FFT预测的基础上做了两个改良效果提升明显。第一将预测结果与一个简单的动量过滤器结合只有FFT预测看多且当前收盘价位于20日均线之上时才做多。第二对频谱做“时间衰减加权”更近历史数据的频率贡献权重更高拉长周期考虑相当于给频谱分析加上了“近期偏好”。这两个改动不属于FFT本身但能让整套策略更贴近市场真实行为。重要提示任何量化策略回测都必须做样本外验证。参数在训练集上调到效果很好不代表在验证集上也能跑通。我习惯至少保留最近一年数据不参与任何调参只在最后做一次完整验证。5. 常见问题与避坑实录5.1 问题速查表问题表现根本原因解决办法频谱图低频处一个高得离谱的峰没去趋势直接对价格FFT先做线性去趋势或对数价格差分预测结果完全是一条直线保留频率过少能量占比阈值太低提高能量阈值到90%以上预测结果剧烈上下抖动保留噪声分量过多降低能量阈值或限制最高频率预测值和实际值总是相差一个固定偏移趋势外推方式不对检查趋势项是否重复叠加或漏加回测方向准确率很好实盘一塌糊涂样本重叠过多导致过拟合滑窗步长设为预测步长减少重叠5.2 端点效应和窗口选择的细节端点效应是FFT预测里最隐蔽的坑。当逆变换重建序列时首尾两端因为边界条件不连续误差明显大于中间位置。而预测恰恰是向外推最后几个点等于误差最大的区域被直接用于决策。缓解端点效应的办法有几个。一是加窗函数比如Hann窗、Hamming窗对序列两端做平滑衰减但代价是序列两端有用的信息被削弱所以预测精度也会打折。二是采用“重叠保留法”每次预测时把窗口适当向后扩预测完只保留最后H个点前面部分舍弃。三是用更长的观察窗口让边界附近的误差相对整体占比更小。我自己测试下来Hann窗会明显降低频谱泄漏但预测结果会变得“偏软”——涨跌幅度都被压缩了这对只看方向、不看幅度的策略影响不大。如果你更关注预测幅度可以考虑只对去趋势残差加窗而不是对完整序列加窗。5.3 关于过拟合和实盘心态说实话用FFT做预测很容易陷入一种“工程满足感”频谱图画得漂亮主频清晰可见重建曲线几乎完美贴合历史走势。但你要清醒地认识到完美拟合历史不代表能预测未来因为FFT拟合的是“历史频率结构”而市场最大的特征就是结构随时会改变。我在某段时间的实盘模拟中发现当行情处于窄幅震荡时FFT预测的方向准确率能接近60%因为震荡本身就是周期波动。但当行情发生突发性突破时FFT预测会完全失效因为它无法识别“新信息”它只把新信息当作噪声滤除掉了。这给了我很深的一个体会FFT适合“慢变量”市场不适合“快变量”冲击任何使用它的人都必须接受这个局限。如果你想进一步提高实战价值建议把FFT当成一个“周期状态指示器”——比如当频谱中某个周期分量的能量显著增强时说明市场进入了该周期的运行节奏这时候结合趋势策略顺势交易效果比单独预测价格好得多。这也是我在后续实践中保留FFT的主要原因它不是一个完美的预测器但确实是一面不错的“照妖镜”能照出行情内部的节奏结构。