用Wind Python接口从零构建Fama-French三因子五因子模型

发布时间:2026/10/25 21:53:24
用Wind Python接口从零构建Fama-French三因子五因子模型 简介这是一份围绕Fama-French三因子与五因子模型的Python实现资料包面向金融量化初学者、金融专业学生以及需要做因子分析的从业者。压缩包共121个文件、约11.98MB以101个py脚本为主辅以csv因子数据表、txt说明、pyc编译文件、xml工程配置和md文档覆盖数据获取、清洗、回归建模与结果输出的完整流程。已有1383人学习适合边阅读代码边理解市场因子、规模因子、价值因子以及盈利因子和投资因子的构造与检验逻辑。资料基于Wind金融终端的数据接口结合pandas、statsmodels等工具完成多因子线性回归可帮助读者快速搭起可运行的因子分析框架并继续扩展到组合构造、收益归因和风险检验等场景。1. Wind_Python-master.zip 在量化里通常指什么先厘清三因子、五因子模型的落地路径如果你在本地磁盘见过 Wind_Python-master.zip 这个压缩包大概率是从某个 Git 仓库或开发者社区拖下来的 Wind Python API 示例工程而 FamaFench 其实是 Fama-French 的常见手误。这个标题真正要解决的事很具体用 Wind 的 Python 接口把行情、市值、财务数据取下来再按 Fama-French1993 三因子、2015 五因子的方法手动构建市场溢价、SMB、HML、RMW、CMA 这五个因子序列最后跑回归看某只股票或某个组合的 alpha 到底是不是运气。这套流程适合量化研究员、金融工程学习者以及手里有一批自选股想验证超额收益是否真实存在的人。读完这一篇你应该能自己从 zip 解压一路跑到回归输出。2. 从 zip 到可运行环境项目结构、依赖安装与 WindPy 初始化2.1 解开 zip 先看什么bin、examples 与 WindPy 的路径约定Wind 官方和第三方维护者发的 Python SDK 包通常不是 PyPI 上那种标准安装包而是一个压缩包。解开之后常见的结构是一个放 WindPy 主体文件的目录里面至少能看到 WindPy 的 pyd 或 py 文件以及配套的数据接口动态库一个 examples 目录放取数和计算的示例脚本可能还有一份 doc 目录放接口说明。不同版本结构略有差异但核心文件基本就那几个。这里有一个容易让新手卡住的点Wind 的 Python 接口不像 requests 那样 pip install 完就结束你需要让 Python 解释器能找到 WindPy 这个模块。常见做法是两种一是把解压出来的主目录放进 PYTHONPATH二是在 site-packages 里写一个 .pth 文件指向这个目录。我一般用第一种简单直接换版本也方便。WindPy 本身没有依赖什么冷门第三方库核心依赖就是 pandas、numpy、scipy、statsmodels其中 statsmodels 是跑因子回归时用的pandas 负责数据对齐。:: 在 Windows 命令行或 PowerShell 里查看 Python 版本确认是 3.7 以上 64 位 python -c import sys; print(sys.version) :: 安装因子模型全套依赖一条命令装完 numpy、pandas、scipy、statsmodels pip install numpy pandas scipy statsmodels :: 把 Wind Python SDK 所在目录写进环境变量注意替换成你的实际路径 set PYTHONPATHD:\wind\Wind_Python-master;%PYTHONPATH% :: 验证 WindPy 能被找到能打印出路径说明环境变量没配错 python -c from WindPy import w; print(w.__file__)这段命令的逻辑是先确认 Python 版本再装依赖然后配环境变量。很多人在 pip 安装 numpy 库的方法上没问题卡在最后一步from WindPy import w直接报 ModuleNotFoundError。这里要特别提醒Wind 官方终端对 Python 位数有要求Python 是 64 位就配 64 位 SDK别混用另外 python 环境变量配置如果没生效新开的命令行窗口也可能不认set PYTHONPATH要重新打开终端再试。2.2 初始化 WindPy 与错误码排查第一次连接要过三道关配好路径之后下一步是启动 Wind 终端并让 Python 进程连上它。Wind 的 Python 接口底层是通过本地协议连到你正在运行的 Wind 金融终端所以终端没打开、没登录Python 这边一定连不上。这个过程在代码里只有两行但报错信息很唬人第一次遇到的几乎都懵。from WindPy import w import time # 启动 Wind 连接返回值 errors 是错误码0 表示成功 errors w.start() print(ErrorCode:, errors) # 如果 ErrorCode 不是 0大概率是终端没开或没登录 if errors ! 0: print(检查 Wind 终端是否已打开并登录) else: # 连续 5 秒每秒检查一次连接状态避免刚启动还没握手完成 for i in range(5): if w.isconnected(): print(连接成功) break time.sleep(1)这里的逻辑是w.start()只负责发起连接返回错误码0代表成功-405这类负数一般是终端未启动或未登录。w.isconnected()是二次确认因为终端刚启动时协议握手可能要一两秒直接取数容易拿到空数据。连接成功后可以先跑一个最小的取数验证比如取上证指数的收盘价确认返回的 ErrorCode 是 0 再往下做。这一道关过了后面的事都是数据处理层面的问题。2.3 第一次取数验证用五只股票确认接口通畅连接验证通过后我习惯用五只不同行业的股票拉一周日线确认w.wsd的返回结构能正确解析。这一步看着简单但能把数据结构问题提前暴露出来比等因子构建到一半再发现日期格式错了要省事得多。from WindPy import w import pandas as pd codes [000001.SZ, 600000.SH, 600519.SH, 000858.SZ, 601318.SH] # wsd 取日频序列pct_chg 是涨跌幅百分比mkt_cap_ard 是总市值元 err, data, times w.wsd(codes, pct_chg,mkt_cap_ard, 2024-01-02, 2024-01-08, PriceAdjF) if err 0: # data 是三维数组股票 × 字段 × 日期需要转置成 DataFrame df_list [] for i, code in enumerate(codes): temp pd.DataFrame({ code: code, pct_chg: data[i][0], mkt_cap_ard: data[i][1], }, indexpd.to_datetime(times)) df_list.append(temp) df pd.concat(df_list) print(df.head())这段代码的关键点是w.wsd返回的data是三维结构第一维是股票第二维是字段第三维是时间序列。很多人直接拿data去构造 DataFrame结果行列全是乱的。times是 Wind 返回的时间列表需要转成pd.to_datetime再当索引。参数PriceAdjF表示不复权因为这里要的是涨跌幅而不是价格本身涨跌幅不受复权影响。如果真的用价格算收益率复权参数就要改成PriceAdjB后复权这个细节第 5 章会专门讲。3. 用 Wind Python 取数行情、市值和财务数据怎么对齐3.1 日频行情用 w.wsd复权参数与返回格式的取舍因子模型的输入分三块日频收益率、截面上的市值和账面价值、以及财务指标。先讲日频行情。Wind 取日频序列的标准接口是w.wsd它支持单股票多字段、多股票单字段、多股票多字段三种组合。多股票多字段时返回三维数组处理起来最绕建议新手拆成单股票循环取速度慢一点但逻辑清晰。from WindPy import w import pandas as pd def fetch_daily(codes, start, end): 批量拉取日频涨跌幅和总市值返回长表 DataFrame frames [] for code in codes: # 单股票取两个字段返回的 data 是 2 × N 的二维结构 err, data, times w.wsd(code, pct_chg,mkt_cap_ard, start, end, PriceAdjF) if err ! 0: print(f{code} 取数失败ErrorCode{err}) continue df pd.DataFrame({ code: code, ret: [x / 100.0 for x in data[0]], # Wind 返回百分比转成小数 mktcap: data[1], }, indexpd.to_datetime(times)) frames.append(df) return pd.concat(frames) # 用法示例拉取 2023 年全年日频数据 df_daily fetch_daily([000001.SZ, 600519.SH], 2023-01-01, 2023-12-31) print(df_daily.head())这里有一个必须养成的习惯Wind 的pct_chg返回的是百分比数值比如涨 2% 返回的是2.0不是0.02。直接拿这个数当收益率跑回归截距和因子系数会整体偏大 100 倍结果完全不可用。我在代码里用x / 100.0做了转换。mkt_cap_ard是总市值单位是元在分组时直接用这个字段排序就行如果你要用流通市值字段名换成mkt_cap_float。3.2 截面数据用 w.wss市值、PB 与账面价值的取法日频数据解决收益率和市值的时间序列但 Fama-French 分组还需要每个股票在特定截面日期的账面价值。账面价值通常用股东权益近似Wind 里可以从w.wss的财务字段取比如tot_equity所有者权益合计。w.wss是截面接口传入一组股票代码和字段返回每个股票在指定日期或最新报告期的值。from WindPy import w import pandas as pd codes [000001.SZ, 600519.SH, 000858.SZ] # wss 取截面数据总市值、市净率、最新一期 ROE err, data, _ w.wss(codes, mkt_cap_ard,pb_lf,roe_ttm, tradeDate20231229) if err 0: df_cross pd.DataFrame({ code: codes, mktcap: data[0], pb: data[1], roe: data[2], }) print(df_cross)tradeDate20231229是截面日期参数表示取这一天收盘后的数据。这里要注意pb_lf是 Wind 的最新市净率roe_ttm是滚动十二个月 ROE。在正式分组时账面价值不能用最新值因为最新值可能包含了未来季报的信息会导致前视偏差。正确做法是固定取上年年报的所有者权益这个逻辑放到 3.3 单独说。如果你在代码里看到别人直接用pb_lf反推账面市值比那套因子序列大概率经不住推敲。3.3 三张表对齐交易日历、停牌与财务披露时点三张表对齐是整个因子模型里最耗时间的环节。第一张是日频行情表第二张是截面市值表第三张是财务数据表。对齐的核心是交易日历Wind 有w.tdays接口可以直接返回两个日期之间的交易日序列比自己去剔除周末和节假日靠谱得多。from WindPy import w import pandas as pd # 取 2023 年全年交易日 err, days, _ w.tdays(2023-01-01, 2023-12-31) trade_cal pd.to_datetime(days) print(交易日数量:, len(trade_cal)) # 行情表按交易日 reindex停牌日会变成 NaN df_daily[trade_date] df_daily.index df_aligned df_daily.set_index(trade_date).reindex(trade_cal) # 停牌日不填充后面算收益率时直接 dropna避免用停牌价算出假收益 df_aligned df_aligned.dropna(subset[ret])停牌是 A 股对齐时最脏的数据。停牌日的价格是前一天的收盘价如果用close的pct_change算收益率复牌那天会算出一个暴涨暴跌的假收益率。正确做法是直接用 Wind 的pct_chg字段它本身已经处理了停牌复牌的涨跌幅口径。如果非要用价格自己算就一定要用后复权价并且把停牌日的收益率置为 0 而不是 NaN。财务数据的对齐则是另一套逻辑每年 6 月底构建组合时只能使用当年 4 月 30 日之前已披露的年报数据这个披露时点的约束比数据本身更重要。4. 手写 Fama-French 因子2x3 分组与 SMB/HML/RMW/CMA 构建全程4.1 分组前准备股票池筛选与因子暴露的原始值分组是 Fama-French 的核心工序。在动手分组前先把股票池洗干净。我的日常做法是股票池用全 A 股剔除 ST、*ST、退市整理、上市不满 12 个月的新股以及分组时点停牌的股票。剔除 ST 和次新股是因为它们的收益特征和正常经营的公司差异太大会把小盘因子带偏剔除停牌股是因为没有可靠价格。import pandas as pd def prepare_universe(df_cross, df_daily, min_list_days252): 筛选股票池剔除 ST、次新、停牌股 # 假设 df_cross 里有 st_flag是否 ST和 list_days上市天数 df df_cross.copy() df df[df[st_flag] 0] # 剔除 ST df df[df[list_days] min_list_days] # 剔除次新股 # 剔除分组日停牌股当日收益率为 NaN 且无成交量的股票 valid_codes df_daily[df_daily[ret].notna()][code].unique() df df[df[code].isin(valid_codes)] return df这段代码的筛选逻辑是三层ST 一条、上市天数一条、停牌一条。list_days字段需要从 Wind 的wss接口取字段名是list_date拿到后自己算上市天数。实际操作里ST 标记也可以用股票名称里是否含 ST 来粗筛但 Wind 有专门的字段is_st直接取来用更可靠。筛选完的股票池在 6 月末时点大概还有 4000 只左右这个量级足够支撑分组计算。4.2 2x3 分组与市值加权组合收益分组边界和权重口径Fama-French 的经典分组是做两次独立的 2x3 切分规模按中位数分成小S大B两组账面市值比按 30%/70% 分位数切成低L、中M、高H三组交叉得到 6 个组合。把这 6 个组合的市值加权收益率算出来后续因子就是这些组合收益的线性组合。import pandas as pd def ff_group(df, size_colmktcap, bm_colbm): 按规模和账面市值比做 2x3 分组返回带组合标签的 DataFrame df df.copy() # 规模分两组中位数以下为 S以上为 B size_med df[size_col].median() df[size_group] df[size_col].apply(lambda x: S if x size_med else B) # 账面市值比分三组30% 分位以下 L70% 以上 H中间 M bm_low, bm_high df[bm_col].quantile([0.3, 0.7]) def bm_group(x): if x bm_low: return L elif x bm_high: return H return M df[bm_group] df[bm_col].apply(bm_group) return df # 组合收益按 size_group bm_group 分组市值加权 def weighted_ret(df, ret_colret, weight_colmktcap): df df.copy() df[weight] df.groupby([size_group, bm_group])[weight_col].transform(sum) df[weighted_ret] df[ret_col] * df[weight_col] / df[weight] port_ret df.groupby([size_group, bm_group])[weighted_ret].sum() return port_ret分组代码里有两个边界坑。第一个坑是分位数口径这里用的是全 A 股中位数而 Fama-French 原论文用的是 NYSE 上市股票的分位数做锚。A 股没有交易所分层常见做法是直接用全 A 分位或者用中证 800 成分股的分位做锚但无论用哪种全流程必须统一否则 SMB 的含义会变。第二个坑是加权方式市值加权是用分组时点的总市值做权重不是等权。等权算出来的 SMB 对小盘股更敏感和学术定义不一致。分组完成后每个组合都是一个市值加权组合组合代码用S_H、B_L这样的标签标识。4.3 计算 SMB/HML/RMW/CMA从六个组合到四组因子序列有了 2x3 组合收益因子就是组合收益的差值。这里要区分三因子和五因子的构建差异三因子只需要规模和账面市值比的分组SMB 是三个小盘组合平均收益减三个大盘组合平均收益HML 是高账面市值比组合平均减低账面市值比组合平均。五因子在此基础上多出盈利分组和投资分组RMW 是高盈利组合平均减低盈利组合平均CMA 是低投资保守组合平均减高投资激进组合平均。# 假设 port_ret 是 DataFrame行索引是组合标签列是月份 # SMB 三因子版S 组三个组合均值 - B 组三个组合均值 smb (port_ret.loc[[S_H, S_M, S_L]].mean() - port_ret.loc[[B_H, B_M, B_L]].mean()) # HML高账面市值比组合 - 低账面市值比组合 hml (port_ret.loc[[S_H, B_H]].mean() - port_ret.loc[[S_L, B_L]].mean()) # RMW 和 CMA 需要额外做盈利/投资分组字段换成 roe 和 invest # RMW 高盈利组合均值 - 低盈利组合均值 rmw (port_ret.loc[[S_R, B_R]].mean() - port_ret.loc[[S_W, B_W]].mean()) # CMA 低投资组合均值 - 高投资组合均值 cma (port_ret.loc[[S_C, B_C]].mean() - port_ret.loc[[S_A, B_A]].mean())这里的因子命名约定是R 表示高盈利RobustW 表示低盈利WeakC 表示保守投资ConservativeA 表示激进投资Aggressive。注意 RMW 和 CMA 的分组也需要先按规模切成 S/B 两组再分别按盈利或投资指标切三组不能用现成的规模组直接套。很多简化代码会把盈利因子直接算成高盈利股票收益减低盈利股票收益不做规模分层这样算出来的因子混入了规模效应和五因子模型里的 RMW 并不是同一种东西。做完因子计算把四组序列存成 CSV这就是后续回归的输入。4.4 做回归三因子与五因子的 alpha 对比因子序列构建好之后回归就是标准的 OLS。这里以单只股票或一个组合的日频收益率为因变量因子收益率为自变量。三因子模型的 X 是市场超额收益、SMB、HML五因子模型在 X 里追加 RMW、CMA。对比两个模型的回归结果重点看两个东西新增因子是否显著t 值绝对值大于 2以及加入后 alpha 是否明显变小。import statsmodels.api as sm def reg_factor(stock_ret, factors, mkt_ret, rf0.0): stock_ret: 个股或组合的超额收益率序列 factors: DataFrame列包含 SMB, HML, RMW, CMA mkt_ret: 市场超额收益率序列 rf: 无风险利率日频一般取 0 或国债收益率/252 y stock_ret - rf X factors.copy() X[MKT_RF] mkt_ret - rf X sm.add_constant(X) # 加截距项截距就是 alpha model sm.OLS(y, X).fit(cov_typeHAC, cov_kwds{maxlags: 5}) return model # 三因子模型 factors_3 df_factors[[SMB, HML]] res_3 reg_factor(stock_ret, factors_3, mkt_ret) # 五因子模型 factors_5 df_factors[[SMB, HML, RMW, CMA]] res_5 reg_factor(stock_ret, factors_5, mkt_ret) # 输出对比alpha、t 值、R2 print(三因子 alpha:, res_3.params[const], t, res_3.tvalues[const]) print(五因子 alpha:, res_5.params[const], t, res_5.tvalues[const]) print(R2 对比:, res_3.rsquared, res_5.rsquared)为什么用cov_typeHAC是关键。日频收益率的回归残差几乎必然存在自相关普通 OLS 的标准误会低估导致 t 值虚高。maxlags5是日频数据常见的滞后阶数选择如果你把数据换成月频可以去掉 HAC 或者把 maxlags 设为 1。截距项const就是 alpha注意这里用的是超额收益做因变量所以 alpha 已经扣掉了无风险利率不要再去减一遍。如果三因子模型里 alpha 显著但五因子模型里不显著说明这个组合的超额收益大概率来自盈利或投资风格暴露而不是真正的选股能力。5. 避坑让三因子五因子复现翻车的 5 个数据细节5.1 现象因子序列和研报对不上复现出来的 SMB 月度均值和研报里的数字差了不止一个量级甚至符号都对不上。原因多半是收益率单位没转换Wind 的pct_chg返回 2.0 代表 2%直接参与计算会被当成 200% 处理所有组合收益和因子收益整体放大 100 倍。解决方法是取数后立刻统一成小数在w.wsd返回处做x / 100.0不要拖到因子构建那一步才处理。另外还要检查复权参数如果自己用价格算收益率前复权和后复权算出来的因子序列在除权除息日附近会明显偏离。5.2 现象SMB 与 HML 高度共线回归系数不稳定回归输出里 SMB 和 HML 的系数在样本区间切换时剧烈抖动VIF 明显偏高。原因基本是分组边界不干净规模分位点用的是全 A 股但账面市值比分位点用的是样本股票自己两套分组锚在部分区间重叠严重。Fama-French 原论文里所有分位点统一用 NYSE 股票计算A 股没有这个条件但至少要把规模和账面市值比的分组锚统一到同一个股票池。解决方法是在 4.2 的分组函数里写死一个anchor_universe参数全流程只用一个股票池算分位点。5.3 现象回归 R2 很高但 alpha 的 t 值忽高忽低日频回归跑出来 R2 有 0.9但把样本区间从一年改成半年alpha 的 t 值就从 2.5 掉到 0.8。原因有两个一是日频残差自相关导致 OLS 标准误不可信二是因子和个股收益率本身都有明显的波动聚集效应。解决方法是回归时用 Newey-West 调整标准误也就是代码里的cov_typeHAC更稳妥的做法是先用日频因子构建月度收益率再做月频回归月频数据的残差自相关问题会轻很多结果也更容易和研报对比。5.4 现象新股和 ST 股把组合收益拉偏某个小盘组合的月收益突然出现极端值查下去发现是刚上市两个月的次新股或者一只摘帽 ST 股贡献的。原因就是股票池筛选没做干净次新股上市初期波动巨大ST 股的交易结构和正常股票完全不同它们混进组合后会把市值加权收益带偏。解决方法是在入池条件里加三条硬性过滤上市天数不少于 252 个交易日、剔除is_st1的股票、剔除分组日成交额为 0 的股票。这三条过滤要放在取数之后、分组之前且每次调仓都重新执行。5.5 现象换月时因子序列跳变前一个月和后一个月不连续6 月底构建的组合7 月初的因子值和 6 月底的因子值之间出现明显断层。原因多半是账面价值的使用时点错了如果用了当年一季报或半年报的最新数据不同股票因为披露时间不同账面价值的新鲜度不一致组合切换时因子就会跳。解决方法是固定使用上年年报的所有者权益并且等年报披露窗口完全关闭4 月 30 日后再换仓。实际操作中我会额外建一个disclosure_map记录每个股票年报实际披露日期确保取到的账面价值在组合构建日之前是公开可得的。6. 把它做成每周可复跑的因子工厂脚本化、缓存与结果校验走到这一步流程已经能跑通但手工执行一遍要花不少时间。更实际的做法是把整个流程写成一个配置驱动的脚本一个config.json维护股票池、起止日期、分组分位数、调仓月份一个主程序按取数 → 清洗 → 分组 → 因子 → 回归的顺序执行。取数结果缓存成 parquet 文件Wind 接口下次就不用重新拉既省时间又避免重复消费接口次数。import json import pandas as pd def run_fama_french(config_path): with open(config_path, r, encodingutf-8) as f: cfg json.load(f) # 从缓存读行情没有缓存则调用 wsd 拉取并存档 df_daily load_or_fetch(daily.parquet, fetch_daily, cfg) # 从缓存读截面数据 df_cross load_or_fetch(cross.parquet, fetch_cross, cfg) # 清洗、分组、计算因子 df_pool prepare_universe(df_cross, df_daily) port_ret ff_group_and_weight(df_pool, df_daily) factors compute_factors(port_ret) # 回归并写日志 res reg_all_stocks(factors, cfg[stock_list]) save_report(res, factor_report.csv) return res脚本化的价值在于每次换股票池、换时间区间只需要改配置不用动代码。校验环节我一般做三件事第一手工抽查三只股票的收益率序列和 Wind 终端界面肉眼核对第二检查 SMB 和 HML 的相关系数经验上绝对值超过 0.4 就要回头查分组逻辑第三对比五因子和三因子的回归结果如果五因子里 RMW、CMA 都不显著且 alpha 变化很小说明这组数据大概率没有问题。我自己在这套流程上踩过最深的坑几乎都是数据对齐和时点问题模型本身的代码从来没有出过错。写模型前先花半天把数据口径理清楚比调参重要得多希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询