简介面向金融量化研究与Python开发者一份基于Wind金融终端的Fama-French三因子与五因子模型实现工程包涵盖从数据获取、因子构造到回归分析的完整链路。包内共121个文件其中101个Python脚本负责数据清洗、因子计算与模型拟合6个CSV文件提供因子与收益数据5个TXT/2个MD文档记录说明与用法另有少量配置与缓存文件压缩包总大小约11.98MB目录组织清晰。已有1383人学习下载适合希望掌握资产定价模型实证编程的读者。借助Wind数据接口与pandas、statsmodels代码示范了市场、市值、账面市值比、盈利与投资风格五类因子的构建方式并给出面板回归、相关性分析和结果输出示例。读者可据此快速复现三因子/五因子回归理解各因子对股票超额收益的解释力同时学习金融数据缺失值处理、收益率匹配等实用技巧。1. 拿到 Wind_Python 因子包先别急着跑三因子、五因子在 A 股的真正落地顺序看到 Wind_Python-master.zip_FamaFench 三因子、五因子模型这个压缩包名字第一反应往往是Fama-French 拼错了。FamaFench 这种写法在 GitHub 和二手转发的项目压缩包里并不少见来源多半是原作者建仓库时手滑下载方也没改。真正值得关注的不是拼写而是这套东西把学术模型和 Wind 数据接口缝在了一起用 Wind Python 拉行情和财务数据按 2×3 分组法切出规模、价值、盈利、投资因子再做时间序列回归。对写 python 量化交易策略代码的从业者来说这是一个可以直接复用的落地模板对刚接触 python 数据分析与可视化的学生它也能把三因子、五因子从公式翻译成可运行的 DataFrame 操作。下面按一条能复现的路径拆开讲。2. 数据底座Wind_Python 拉什么字段、怎么清洗才喂得饱因子模型因子模型回归需要两类数据组合收益率序列和因子收益率序列。组合分到哪个桶取决于市值、市净率、盈利和投资四个特征这些特征在 Wind 里分布在行情接口和财务接口两个域。常见做法是用 WindPy 的 w.wsd 拉时间序列w.wss 拉某一交易日的截面快照w.wset 拉股票池或指数成分。别指望一个函数拿全所有数据Wind 接口的限制更多体现在字段数量上冗余字段请求会让接口超时这在 python 入门阶段最容易踩坑。2.1 环境准备python 安装、Wind 终端登录与 WindPy 连通WindPy 是 Wind 本地终端的一个 Python 封装本质上通过本地通信调用终端里的数据服务。所以第一步不是 pip install而是确认这台机器上有已登录的 Wind 金融终端。最小可用的连接代码如下from WindPy import w w.start() if not w.isconnected(): raise RuntimeError(Wind 终端未连接请先打开终端并完成登录)逻辑说明w.start()初始化与终端的通信通道w.isconnected()返回布尔值。终端没开、共享模式没开、账号权限里不含量化数据包时isconnected()都会是 False。另一个容易翻车的点是位数不匹配WindPy 的 dll 依赖终端位数Windows 上建议终端和 python 都保持 64 位。python 安装教程里常见的版本选择问题在这里一样存在3.8 到 3.11 的 64 位版本我都跑过重点不是最新而是和本机终端版本兼容。这一步可以顺手把后面要用的库装齐pip install pandas numpy statsmodels openpyxl matplotlibpandas 负责因子分组和面板对齐numpy 处理数组切片和分位计算statsmodels 做 OLS 回归openpyxl 用于把结果导出成 Excelmatplotlib 画累计净值曲线。这些都是做 python 数据分析与可视化最常用的一套组合。2.2 行情与截面取数w.wss 和 w.wsd 的配合方式先准备一个股票池。常见做法是读取本地 csv或者用 w.wset 按指数成分取一遍import pandas as pd pool pd.read_csv(stock_pool.csv, dtype{code: str}) codes pool[code].tolist() info w.wss(codes, mkt_cap_float,val_pb_lf,close, tradeDate20231229;currencyTypeCNY) snapshot pd.DataFrame({ code: codes, mv: info.Data[0], pb: info.Data[1], close: info.Data[2], }) snapshot[bp] 1 / snapshot[pb]逻辑说明w.wss返回一个对象Data是一个列表每一项对应用户传入的一个字段顺序和字段顺序一致。上面这段把流通市值、市净率、收盘价拉回来组成了截面快照bp用1/pb近似。字段名在不同 Wind 版本里可能略有差异如果val_pb_lf取不到可以换成pb或bps/close手工计算。跑通之前不要盲信字段名先用help(w.wss)或终端里的字段浏览器确认。组合收益需要日线行情用w.wsd取price w.wsd(codes, close, 2023-01-01, 2023-12-31, FillPrevious;PriceAdjF;CycleD)FillPrevious让停牌日沿用前收盘价这样计算收益率时不会出现空值PriceAdjF是前复权。回测里更稳妥的是后复权取决于你关注的是价格涨幅还是实际可成交价格前者适合因子排序后者适合策略模拟。2.3 因子模型的最小字段清单与清洗顺序把因子模型需要的最小数据集合固定下来字段用途清洗与备注流通市值 mkt_cap_float规模因子 SMB 分组剔除停牌、ST、上市未满 60 天的新股市净率 val_pb_lf价值因子 HML 分组bp 1/pb剔除 pb0 的样本毛利或 ROE盈利因子 RMW 分组用最近已公告财务数据不能直接用报告期总资产或总资产同比投资因子 CMA 分组同比口径需对齐公告日期收盘价/涨跌幅组合收益率月度调仓使用持有期收益率清洗顺序建议先过滤股票池再处理极端值再分组。不要先把所有月份的数据 concat 成大表再统一切分位点那样会把不同月份的股票混到一个切分区间里后面会专门讲这个错位问题。BP 出现几百上千的极端值会直接破坏 30/70 分位点所以要在分组前对 bp 做 1% 和 99% 的 winsorize也就是把超出分位的值压缩到边界值。极值处理不是统计学洁癖它是让分组结果不被几只有财务异常的股票绑架的必要操作。3. 三因子模型落地2×3 分组法构造 SMB 与 HML再用 OLS 验证三因子模型是五因子模型的骨架。核心就一句话用两个维度把股票切成六格等权平均后构造两个多空价差。市场因子用宽基指数收益近似规模因子 SMB 是小市值组合减大市值组合价值因子 HML 是高账面市值比组合减低账面市值比组合。3.1 构造 2×3 组合先切 S/B再切 L/M/H在每个调仓日对同一截面的股票做三件事按流通市值中位数分成 S 和 B按 bp 的 30/70 分位分成 L、M、H然后交乘成六个篮子计算下一个持有期的等权收益。这里的关键是中位数和分位点必须在同一个截面内部计算。def assign_2x3(df): df df.dropna(subset[mv, bp, ret]) df[size_p] np.where(df[mv] df[mv].median(), B, S) bplo df[bp].quantile(0.30) bphi df[bp].quantile(0.70) df[val_p] M df.loc[df[bp] bplo, val_p] L df.loc[df[bp] bphi, val_p] H return df逻辑说明median()和quantile()都只针对传进来的这个月的截面数据。np.where做市值二分组loc按条件把 bp 分成三类。返回值里的size_p和val_p就是后面分组的标签列。注意df必须已经带上ret字段也就是下一个月持有期收益率否则 dropna 会把这一层过滤掉。我自己习惯在算收益时用下一期收益而不是当期收益。也就是说T 日收盘后拿到市值和 BP用 T1 到 T2 的收益作为持有期收益这样避免把当天已经实现的价格变动当成因子信号。3.2 因子收益率序列SMB 是市值价差HML 是价值价差六个篮子都算完等权平均后按公式合并def ff3_factors(df): grouped df.groupby([month, size_p, val_p])[ret].mean() g grouped.unstack([size_p, val_p]) smb (g[(S, L)] g[(S, M)] g[(S, H)]) / 3 \ - (g[(B, L)] g[(B, M)] g[(B, H)]) / 3 hml (g[(S, H)] g[(B, H)]) / 2 \ - (g[(S, L)] g[(B, L)]) / 2 return smb.rename(smb).to_frame().join(hml.rename(hml))逻辑说明groupby后unstack得到以月份为索引、以 size_p 和 val_p 为列 MultiIndex 的 DataFrame。SMB 是六个小盘组合的平均减六个大盘组合的平均HML 是把大小市值两组里的 H 和 L 分别平均后做差。HML 用两组平均而不是直接拿一个组合是为了让多头和空头的市值结构对称避免 HML 里混入规模效应。如果 unstack 之后出现 NaN说明某个月某个篮子没有股票常见原因是股票池太小或过滤条件太激进。此时直接 fillna(0) 会掩盖问题正确做法是回到过滤逻辑看是不是切掉了太多样本。3.3 OLS 回归用 statsmodels 验证载荷与 alpha因子构造完成后做三因子时间序列回归。被解释变量是一个待测试组合的超额收益解释变量是市场因子、SMB、HML 的收益率序列。import statsmodels.api as sm panel portfolio_monthly.merge(mkt_factor, onmonth) panel panel.merge(ff3, onmonth).dropna() panel[ex_ret] panel[portfolio_ret] - rf_monthly X sm.add_constant(panel[[mkt_rf, smb, hml]]) model sm.OLS(panel[ex_ret], X).fit() print(model.params.round(4)) print(model.tvalues.round(3)) print(fadj R2: {model.rsquared_adj:.3f})逻辑说明portfolio_ret是策略或组合的月度收益率rf_monthly是无风险利率的月度近似mkt_rf是市场指数收益减无风险利率。Ols 回归得到的常数项就是 alphaSMB 和 HML 的系数是风格暴露t 值用来判断暴露是否显著。一个常见的理解误区如果把全 A 等权收益作为被解释变量市场因子的解释力接近 1alpha 接近 0R² 接近 1这是数学恒等式不代表模型拟合得好。检验因子模型有没有用被解释变量必须换成策略净值、行业指数或者某个单独构建的多空组合。我一般会同时跑宽基 ETF 和自研策略两版对比 alpha 和载荷差异。回归样本至少需要 12 到 24 个月少于这个长度t 值基本没有参考价值。4. 五因子扩展RMW 与 CMA 的构造逻辑和 A 股数据适配Fama-French 五因子模型在美股市场能解释更多截面收益但放在 A 股RMW 和 CMA 的表现跟三因子很不一样。加这两个因子最需要关注的是财务字段的公告时点而不是回归代码本身。4.1 构造 OP 和 INV 特征从财务原始值到分组标签RMW 对应的是盈利因子学术原版用营业利润减相关成本后除以账面权益。国内实现常用简化口径毛利除以总资产或者直接使用 ROE。常见做法是取最新已公告财报的 ROE因为 ROE 口径在各数据库之间更统一也避开了营业利润项目在不同行业里的口径差异。INV 对应投资因子经典口径是总资产同比增长率。这里有个细节总资产增长率需要两期财报数据而两期数据的公告日不同不能简单按报告期 merge 后直接相减。跨季比较之前先按公告日对齐。def add_op_inv(df_fin, df_snapshot): df_fin df_fin.sort_values(ann_date) df_snapshot df_snapshot.sort_values(trade_date) merged pd.merge_asof( df_snapshot, df_fin, left_ontrade_date, right_onann_date, directionbackward ) merged[op] merged[gross_profit] / merged[total_assets] merged[inv] merged[total_assets_ttm].pct_change() # 实际用同比更稳 return merged逻辑说明merge_asof会把每个调仓日匹配到「最近一次已经公告」的财务数据避免把还没公布的数据提前用进去这是消除未来函数的关键一步。directionbackward表示只能往过去找不能向未来取数。op 用毛利除以总资产做简化inv 用总资产的同比变化pct_change 在这里只是示意实际应该用上年同期值算同比。4.2 五因子分组与 RMW、CMA 因子序列五因子并不要求做三重交乘。业界常用做法是在同一截面里对 OP 和 INV 分别按 30/70 分位切三组然后分别构造 RMW 和 CMA。下面这个函数沿用前面的 2×3 框架def build_rmw_cma(df): df[op_p] np.where(df[op] df[op].quantile(0.7), R, np.where(df[op] df[op].quantile(0.3), W, M)) df[inv_p] np.where(df[inv] df[inv].quantile(0.7), A, np.where(df[inv] df[inv].quantile(0.3), C, M)) g df.groupby([month, size_p, op_p, inv_p])[ret].mean() g2 g.unstack([size_p, op_p, inv_p]) def two_side(bucket): return (g2[(S, bucket, C)].fillna(0) g2[(S, bucket, A)].fillna(0) g2[(B, bucket, C)].fillna(0) g2[(B, bucket, A)].fillna(0)) / 4 rmw two_side(R) - two_side(W) cma two_side(C) - two_side(A) return pd.concat( [rmw.rename(rmw), cma.rename(cma)], axis1 )逻辑说明op_p的三个标签 R、W、M 分别对应高盈利、低盈利、中间组inv_p的 A、C、M 对应高投资、低投资、中间组。two_side把每个因子的首尾两组分别汇总到四个交乘篮子上取平均让市值和另一维度都尽量均衡。fillna(0) 在这里是兜底如果某个篮子缺失太频繁因子序列会失真这时要回头查op和inv的空值比例。4.3 五因子回归与多重共线性诊断五因子回归的代码和三因子几乎一样只是 X 里多了 rmw 和 cma。真正要留意的是因子相关性。A 股里 HML 与 RMW、CMA 经常表现出负相关回归中某个载荷符号异常先别急着判断代码错了看相关矩阵和 VIFcorr panel[[mkt_rf, smb, hml, rmw, cma]].corr() print(corr.round(3)) from statsmodels.stats.outliers_influence import variance_inflation_factor X sm.add_constant(panel[[mkt_rf, smb, hml, rmw, cma]]) vif pd.Series( [variance_inflation_factor(X.values, i) for i in range(X.shape[1])], indexX.columns ) print(vif.round(2))逻辑说明相关系数能直接看出哪两个因子共线VIF 大于 10 说明该因子基本可以被其他因子线性表达。遇到这种情况一般做法是去掉其中一个因子或者先把因子正交化再做回归而不是硬解释回归系数。5. 因子模型避坑手册Wind 连接、未来函数与分组错位的排查三因子、五因子模型听起来简单实际落地的坑集中在数据层和分组逻辑。下面按现象到原因到解决的顺序写这些基本都是我自己在跑因子时踩过的。5.1 w.start() 返回空接口一直连不上现象w.start()没有报错但w.isconnected()一直是 False或者第一次调用w.wss时直接返回错误码。原因Wind 终端没有登录或者当前账号没有数据权限又或者终端共享模式未开启。多数情况是终端最小化到托盘、连接被挂起。解决手动打开 Wind 金融终端并完成登录确认量化接口权限已开通。如果终端和 python 分属 32 位和 64 位也会出现连上但不返回数据的情况把两者统一成 64 位即可。5.2 SMB、HML 序列和直觉完全相反月度符号反复横跳现象HML 长期为负SMB 正负交替没有规律因子月度收益看起来像白噪声。原因分组用的市值和 bp 分位点被跨月份混合计算了。很多人把十二个月的截面 concat 成一个 DataFrame再统一quantile(0.3)导致上个月的股票被这个月的分位点切分。解决每个调仓日独立计算中位数和分位点。上面的assign_2x3函数已经按这个逻辑做了不要在传入前 concat 多个月份数据。5.3 财务数据用了报告期回测 alpha 高得离谱现象五因子回归的 alpha 显著为正而且大得超出合理范围滚动回测曲线几乎不亏钱。原因分组时把报告期的财务数据直接对齐到了报告期末但财报实际是在报告期结束后两到四个月才公布。回测在信息公布前就使用了它等于开了天眼。解决用公告日期ann_date做 merge_asof确保每次调仓只使用已经公告的数据。如果拿不到公告日期宁可把财务数据统一延迟三个月使用。5.4 停牌、涨跌停、ST 股把组合收益带偏现象某个月组合收益被一两只股票主导换仓时发现根本买不进或卖不出。原因停牌股用前收盘价计算收益复牌后的跳空会直接砸进持有期收益涨跌停股在调仓日无法成交ST 股和次新股的基本面波动也不适合直接进因子池。解决取行情时用FillPrevious填充停牌日同时剔除 ST、退市整理股和上市不足 60 天的次新股。因子计算时可以保留它们但真正的策略回测必须过滤。5.5 因子载荷符号与学术结论相反现象RMW 载荷显著为负或者 CMA 因子对组合没有解释力。原因A 股风格与美股不同盈利因子和投资因子的溢价方向并不稳定加上 HML 与 RMW 的相关性较高回归系数被互相拉扯。解决先看单因子多空组合的收益再进多因子回归。如果单因子本身没有稳定溢价放进五因子模型里也很难有合理表现。这种情况不是代码 bug是市场特征。6. 把三因子和五因子做成滚动监控36 个月滚动回归、缩尾与 Excel 报告单次全样本回归只能看平均关系看不到因子时效性。我一般会把因子模型做成滚动监控每 36 个月跑一次回归记录 alpha 和载荷观察它们随时间的变化。这样比一次全样本回归更有工程价值。def rolling_ff5(panel, window36): rows [] for end in range(window, len(panel) 1): sub panel.iloc[end - window:end] if sub[ex_ret].std() 0: continue X sm.add_constant(sub[[mkt_rf, smb, hml, rmw, cma]]) fit sm.OLS(sub[ex_ret], X).fit() row {month: sub.index[-1]} row.update(fit.params.to_dict()) row[t_alpha] fit.tvalues.get(const, np.nan) row[adj_r2] fit.rsquared_adj rows.append(row) return pd.DataFrame(rows).set_index(month)逻辑说明window36表示每次用 36 个月做滚动回归sub.index[-1]作为结果月份方便把载荷序列画出来看变化。样本不足 36 个月时前面几行会缺失这是正常的。滚动回归比全样本回归更能暴露因子失效期比如某段时间 alpha 连续为负说明策略风格与市场环境背离。最后把滚动结果导出成 Excel方便团队其他人直接看with pd.ExcelWriter(ff5_rolling.xlsx, engineopenpyxl) as writer: rolling.to_excel(writer, sheet_namerolling_alpha) factor_corr.to_excel(writer, sheet_namefactor_corr)这里用 openpyxl 引擎是因为 pandas 默认的 xlsxwriter 在部分环境里没装。导出前先确认 rolling 索引不是重复的否则 Excel 写入会报错。做因子模型这几年我养成了一个习惯任何新拿到的 Wind_Python 因子包先不看策略收益先跑一遍数据正确性校验再跑滚动回归确认 alpha 和载荷在时间上是稳定而不是一根直线。代码能跑起来只是起点数据口径对得上才是真正能用的方案。希望帮到你。本文还有配套的精品资源点击获取