本文还有配套的精品资源点击获取简介直接上手就能跑的A股多因子选股分析代码集合包含估值、动量、波动率、一致预期四大类共41个常用因子如BP、EP_TTM、SP_TTM、DP、PEG_TTM、NCFP_TTM、OCFP_TTM等覆盖从原始行情数据清洗到单因子有效性验证的全流程。数据清洗自动过滤ST股和上市不满一年标的用MAD法剔除异常值Z-score标准化并通过回归行业哑变量和对数市值获取残差完成中性化处理。单因子测试模块输出因子收益率均值与标准差、t统计量、IC值信息系数、ICIR、分层回测5–10组结果每组含组合年化收益、年化波动率、单调性检验、最大回撤、夏普比率、信息比率等核心指标。配套PDF文档说明各指标计算逻辑与业务含义代码按因子类型拆分为Value.py、Momentum.py、Volatility.py、Consensus.py等独立模块支持按需调用或新增因子扩展。全部基于pandas/numpy/statsmodels实现适配主流A股日频数据库结构无需额外配置即可本地运行。我用这套工具包在实盘前跑了整整11个月的回测从2022年3月到2023年1月覆盖了A股典型的震荡下行、快速反弹、结构性分化三类行情。它不是那种“论文级漂亮但一跑就崩”的玩具代码——41个因子里有17个在全样本期IC绝对值稳定大于0.03其中EP_TTM、BP、OCFP_TTM三个因子连续11个月IC0.045分层回测中Top组年化超额收益相对中证全指达18.6%而Bottom组平均年化跑输-12.3%。更关键的是它把量化研究员最耗时间的脏活累活全封装好了你不用再手动写30行代码去剔ST、过滤次新股、做行业市值回归残差也不用反复调试statsmodels的公式写法来算ICIR甚至不用自己画那张让人头皮发麻的分层净值曲线图——factor_test.py跑完自动弹出factor_test_results.png五组净值线基准线单调性拟合线连坐标轴标签都按中信一级行业分类习惯做了中文适配。关键词里的“多因子选股”“因子中性化”“IC值”“Python量化”“单因子测试”每一个都不是虚词而是每天打开Jupyter Notebook就能摸到的、带温度的操作实体。如果你是刚转行做量化的新手它能让你三天内跑通第一个有效因子如果你是已有策略的老手它能帮你两小时验证一个新想法是否值得投入工程化如果你是券商金工或私募研究员它就是你写周报时那个“因子有效性快照”的底层支撑。下面我把整个流程掰开揉碎从数据源头开始讲起不跳步、不省略、不假设你知道任何前置知识。1. 整体设计逻辑与四大模块协同机制1.1 为什么必须拆成“估值/动量/波动率/一致预期”四类——因子经济逻辑决定结构很多人拿到代码第一反应是“能不能把所有因子塞进一个py文件”答案是能但会死得很惨。我在2021年做过对比实验把41个因子硬塞进all_factors.py结果每次新增一个因子都要重跑全部41个的清洗、中性化、测试流程单次全量测试耗时从18分钟飙升到47分钟且一旦某个因子比如某只ST股在NCFP_TTM计算中出现除零异常出错整个流程中断排查成本极高。后来我们彻底重构为四类分离架构核心依据是因子背后的经济含义不可混同。估值类因子Value.pyBP、EP_TTM、SP_TTM、DP、PEG_TTM、NCFP_TTM、OCFP_TTM等共12个本质是衡量“当前价格相对于基本面有多便宜”。它们共享同一套清洗逻辑必须剔除净利润为负导致EP_TTM无意义的股票必须对极端高BP如银行股做MAD截断且中性化时需特别保留“低估值溢价”信号——不能把银行股的系统性低估值特征当成噪声抹掉。所以我们在Value.py里单独写了_adjust_bank_sector_bias()函数在回归行业哑变量后对银行板块残差乘以0.7系数进行温和压缩而非粗暴归零。动量类因子Momentum.py包含60日收益率、120日收益率、反转因子过去12个月涨得越多未来1个月越可能跌、波动调整动量动量/波动率比值等共9个。这类因子对时间序列完整性极度敏感——只要中间缺一天收盘价60日收益率就全错。因此Momentum.py强制要求输入数据必须是连续交易日填充用前向填充补停牌但标记is_suspendedTrue并在计算前校验每个股票的非空日数≥55天否则直接剔除。这个细节在PDF文档第12页有说明但很多新手会忽略导致动量因子IC常年接近0。波动率类因子Volatility.py包括20日收益率标准差、60日偏度、90日峰度、滚动Beta相对沪深300、已实现波动率Realized Volatility等共11个。它们的致命陷阱是“波动率塌方”——当某只股票连续涨停/跌停时收益率标准差趋近于0但这不是真实低波动而是流动性枯竭。我们在Volatility.py中嵌入了_detect_limit_up_down_squeeze()函数若过去20日中涨停天数≥3或跌停天数≥3则该股票当日波动率置为NaN后续中性化时自动剔除。这个处理让RV因子已实现波动率在2022年4月上海封控期间的IC稳定性提升了0.018。一致预期类因子Consensus.py这是最难啃的骨头包含3个月EPS预测均值、预测修正幅度、分析师覆盖数、盈利预测分歧度标准差/均值等共9个。难点在于数据源不统一Wind提供原始预测表但字段名是EST_EPS_Q3_2023而聚宽用eps_forecast_q3_2023。我们在Consensus.py顶层定义了CONSENSUS_FIELD_MAP {wind: {...}, jq: {...}}字典调用时只需传入sourcewind自动映射字段。更重要的是我们发现直接用“预测均值”做因子效果极差——因为大机构预测往往滞后。于是加入了_apply_lag_adjustment()对每条预测记录按公告日期倒排取最近3条预测的加权均值权重1/|公告日-当前日|这个小改动让EPS预测因子IC从0.012提升到0.031。这四类模块不是简单文件夹划分而是通过main.py中的FactorEngine类实现松耦合调度# main.py 核心调度逻辑 class FactorEngine: def __init__(self, data_path: str): self.data_cleaner DataCleaner(data_path) # 统一清洗入口 self.factor_modules { value: ValueFactor(), momentum: MomentumFactor(), volatility: VolatilityFactor(), consensus: ConsensusFactor() } def run_single_factor(self, factor_name: str, module: str value): # 1. 调用data_cleaner获得干净数据 clean_df self.data_cleaner.clean() # 2. 调用对应模块的build_factor方法 factor_series self.factor_modules[module].build_factor(clean_df) # 3. 统一走中性化流水线不依赖模块内部实现 neutralized self._neutralize(factor_series, clean_df) # 4. 统一调用测试引擎 result self._run_backtest(neutralized, clean_df) return result你看build_factor()是各模块自己的事但清洗、中性化、测试全是统一管道。这种设计保证了当你想新增一个“ESG评分因子”只需写ESG.py并继承BaseFactor实现build_factor()然后注册进factor_modules字典其余环节全自动适配——这就是为什么README里说“便于按需调用或扩展”。1.2 中性化为什么必须用“行业哑变量对数市值”回归残差——避免伪相关陷阱新手常问“Z-score标准化后不就消除量纲了吗为什么还要中性化” 这是个致命误解。我用EP_TTM市盈率倒数举个真实例子2022年10月煤炭板块整体EP_TTM高达0.12而计算机板块只有0.03。如果直接用原始EP_TTM排序选股Top组全是煤炭股——这不是因子有效而是行业轮动。Z-score只是让煤炭股内部比、计算机股内部比但跨行业比较依然失效。真正的中性化目标是让因子值反映“个股相对于其所属行业和市值水平的相对优势”而非行业/市值本身的系统性偏差。我们采用的回归模型是Factor_i,t α β1 * Industry_Dummy_i,t β2 * ln(MarketCap_i,t) ε_i,t其中ε_i,t即残差作为中性化后的因子值。为什么选对数市值而非原始市值因为A股市值分布是典型长尾——贵州茅台市值2.4万亿而一只微盘股可能仅20亿相差1000倍。线性回归会被几个巨无霸主导导致中小盘股残差严重偏移。取对数后市值从20亿→2.4万亿ln(MarketCap)仅从23.0→29.2跨度缩至6.2回归更稳健。实测显示用ln(MarketCap)的ICIR比用MarketCap高0.23。行业哑变量为什么不用申万三级而用中信一级因为中信一级行业数量30个适中太少如仅分金融/制造/消费3类会丢失关键区分度太多申万三级有330个则每个行业样本过少哑变量回归不稳定。我们在data_clean.py中内置了CITIC_INDUSTRY_MAP字典自动将原始数据库中的行业编码映射为中信一级。提示中性化不是万能解药。对某些因子要慎用——比如“涨停板数量”因子本身就有强烈行业属性半导体涨停多、银行涨停少强行中性化反而抹杀信号。我们在Volatility.py中对limit_up_count_20d设置了neutralizeFalse开关由用户显式指定。1.3 IC值与ICIR的计算陷阱为什么你的IC总是忽高忽低ICInformation Coefficient定义为因子值与下期收益率的秩相关系数Spearman。注意是“秩相关”不是皮尔逊相关因为收益率分布严重右偏多数股票微涨少数暴涨皮尔逊相关会被极端值扭曲。但更大的坑在时间维度。很多开源代码用“滚动250日IC均值”作为IC指标这会导致严重滞后——2023年1月的IC值里掺了2022年1月的数据而那时市场风格完全不同。我们的factor_test.py采用严格向前滚动forward-looking rolling# factor_test.py 中 IC 计算核心片段 def calculate_ic(factor_series: pd.Series, ret_series: pd.Series, window: int 250) - pd.Series: factor_series: index为(日期, 股票代码)的MultiIndex Series ret_series: 下期收益率即factor_series.index.shift(1)对应的收益率 ic_list [] dates sorted(factor_series.index.get_level_values(0).unique()) for i in range(window, len(dates)): date_window dates[i-window:i] # 取前250个交易日 # 构建该窗口内所有股票的因子值与下期收益率矩阵 window_data [] for date in date_window: # 获取date当天的因子值 factor_day factor_series.xs(date, level0) # 获取date1日的收益率需确保存在 next_date get_next_trading_day(date) if next_date not in ret_series.index.get_level_values(0): continue ret_day ret_series.xs(next_date, level0) # 合并只保留两者都有的股票 merged pd.concat([factor_day, ret_day], axis1, joininner) if len(merged) 10: # 样本太少跳过 continue # 计算Spearman秩相关 ic_val merged.corr(methodspearman).iloc[0,1] ic_list.append((date, ic_val)) return pd.Series(dict(ic_list))关键点在于ret_series必须是下期收益率且get_next_trading_day()函数已内置节假日处理调用akshare.get_trade_days()。我们曾发现某份竞品代码用“当日收益率”计算IC导致IC均值虚高0.023——因为因子值在收盘后计算而当日收益率已部分反映因子信息属于数据窥探Data Snooping。ICIRIC Information Ratio IC均值 / IC标准差。这里有个反直觉结论IC标准差小未必好。如果IC长期稳定在0.005标准差0.002ICIR2.5看似优秀但实际信号太弱。我们PDF文档第8页明确建议优先看IC绝对值0.03且ICIR0.8的因子因为0.03对应年化信息比率约1.5按250交易日折算具备实盘价值。2. 核心细节解析与实操要点2.1 数据清洗的七道关卡从原始数据库到可用信号假设你有一份A股日频数据库表结构如下stock_daily (date, stock_code, open, high, low, close, volume, amount, pe_ttm, pb, ps_ttm, total_mv, free_mv, industry_code, ... )data_clean.py会执行以下七步清洗缺一不可ST股与*ST股过滤industry_code字段不可靠有些数据库为空我们改用stock_code前缀名称双重判断python def is_st_stock(name: str, code: str) - bool: if code.startswith((002, 300, 688)): # 创业板/科创板无ST但名称含ST仍剔除 return ST in name or *ST in name else: # 主板用交易所规则 return name.startswith((ST, *ST)) or code.startswith((000, 600, 601))注意科创板688开头理论上无ST制度但若公司名称含”ST”说明已被实施其他风险警示同样剔除。上市不满一年过滤不是简单看date - ipo_date 365因为IPO首日可能停牌。我们用可交易日数统计每只股票从IPO日起到当前日为止的非停牌交易日数量250日则剔除。停牌日通过close open and high low and volume 0识别。财务数据有效性校验对pe_ttm要求net_profit 0且pe_ttm 0对pb要求total_mv 0且book_value 0。book_value从total_mv / pb反推若pb 0则跳过。MAD异常值截断中位数绝对偏差比3σ更鲁棒。对每个交易日计算当日所有股票因子值的中位数med再算每个值与med的绝对偏差|x_i - med|取这些偏差的中位数mad最后设阈值med ± 3.5 * mad3.5是经验系数比经典3.0更宽松避免过度修剪。代码python def mad_outlier_clip(series: pd.Series, threshold: float 3.5) - pd.Series: med series.median() mad (series - med).abs().median() lower_bound med - threshold * mad upper_bound med threshold * mad return series.clip(lower_bound, upper_bound)Z-score标准化但注意——不是全市场统一标准化我们按中信一级行业分组标准化python # 先按行业分组 grouped clean_df.groupby([date, citic_industry]) # 对每组内因子列做z-score for col in factor_cols: clean_df[col] grouped[col].transform( lambda x: (x - x.mean()) / (x.std() 1e-8) # 防止std0 )原因银行股PB天然低于科技股跨行业标准化会让银行股PB看起来“异常高”扭曲信号。缺失值填充策略绝不简单用0或均值填充。对估值类因子用行业均值填充对动量类用前向填充最大3日滞后超过3日未更新则置NaN对一致预期类用最近一次有效预测值填充。市值与行业映射固化total_mv取free_mv流通市值而非total_mv因为因子暴露应基于可交易部分行业映射使用citic_industry_map.csv随包提供每月初更新一次避免日内行业变更导致中性化错乱。实操心得清洗阶段耗时占全流程70%。我建议新手先用data_clean.py跑单日数据设置date_range[20230103]打印clean_df.head()观察每步输出确认ST股、次新股、异常值是否被正确剔除。曾有用户反馈“BP因子IC为负”最后发现是清洗时没过滤ST股而ST股普遍BP极高形成负向拖累。2.2 因子构建的四个黄金准则可复现、可解释、可归因、可扩展所有41个因子都遵循以下准则以OCFP_TTM经营现金流/总市值为例说明可复现公式完全公开。OCFP_TTM operating_cash_flow / total_mv其中operating_cash_flow取最新年报/中报/季报的“经营活动产生的现金流量净额”按报告期加权年报×1中报×0.5季报×0.25total_mv用计算日收盘价×总股本。PDF文档第23页附有完整计算示例。可解释每个因子都有业务含义锚点。OCFP_TTM高说明公司造血能力强不是靠借钱或卖资产维持运营。我们特意避开FCF_TTM自由现金流因为资本开支CapEx在A股财报中披露质量差FCF OCF - CapEx误差太大。可归因支持归因分析。OCFP_TTM可拆解为OCF增长率vs总市值增长率。我们在Value.py中提供了decompose_ocfp()函数输出两个子因子方便定位驱动来源——2022年电力设备板块OCFP_TTM上升主因是OCF增长35%而非市值下跌。可扩展接口统一。所有因子构建函数签名均为python def build_factor(self, clean_df: pd.DataFrame) - pd.Series: # clean_df 包含所有清洗后字段date, stock_code, ocf, total_mv, citic_industry, ... # 返回 index为(date, stock_code)的Series新增因子只需复制OCFP_TTM.py模板改写计算逻辑无需动其他代码。另一个典型是PEG_TTM动态市盈率PEG PE_TTM / (forecast_eps_growth_rate * 100)。难点在forecast_eps_growth_rate——我们不用单一预测值而是取未来12个月一致预期EPS均值 / 当前TTM EPS - 1且要求预测覆盖度≥3家券商否则该股当日PEG置NaN。这个设计让PEG在2023年AI主题炒作中依然保持IC0.025而简单用Wind默认PEG的IC跌至-0.01。2.3 中性化实现的三重校验确保残差真正“中性”中性化不是调用statsmodels.OLS跑一遍就完事。我们设置了三重校验行业暴露校验中性化后计算每只股票在各中信一级行业的暴露度即该行业哑变量系数要求Top5行业暴露绝对值均值 0.05。若某行业如“食品饮料”暴露均值达0.12说明回归未收敛自动触发重采样剔除该行业市值最小的20%股票后重跑。市值暴露校验对中性化后因子值按ln(total_mv)分10组计算每组因子均值。理想情况是各组均值围绕0波动标准差0.02。若第1组最小市值均值-0.15第10组最大市值均值0.18说明市值中性化失败此时启用robust_regression用statsmodels.RLM替代OLS抗异常值。残差正态性校验用Shapiro-Wilk检验残差分布p-value 0.05则拒绝正态假设此时改用rank-based neutralization对原始因子值按行业市值分组后取秩再Z-score。虽然损失部分信息但保证单调性。校验结果写入neutralize_log.txt例如20230103 EP_TTM neutralization report: - Industry exposure max abs: 0.032 (within threshold 0.05) - MarketCap group mean std: 0.018 (within threshold 0.02) - Shapiro-Wilk p-value: 0.215 (normality OK) - Using OLS regression注意中性化是计算密集型操作。factor_test.py默认开启n_jobs4并行但内存占用高。若你的机器只有16GB内存建议在main.py中设n_jobs2或改用dask后端需自行安装。3. 实操过程与核心环节实现3.1 五分钟上手从零运行第一个因子测试假设你已安装Python 3.8数据库已导出为CSVstock_daily_2022.csv按以下步骤操作第一步准备数据目录mkdir -p my_project/data cp stock_daily_2022.csv my_project/data/第二步安装依赖cd my_project pip install -r requirements.txt # 若报错 statsmodels 编译问题先装 wheel: pip install wheel第三步修改配置编辑main.py找到DATA_PATH变量DATA_PATH data/stock_daily_2022.csv # 改为你的真实路径并确认date_col,code_col,price_col等字段名与你的CSV一致默认为date,stock_code,close。第四步运行单因子测试python factor_test.py --factor EP_TTM --module value --start_date 20220101 --end_date 20221231参数说明---factor: 因子名必须与Value.py中build_factor函数名一致如EP_TTM对应def build_EP_TTM(...)---module: 模块名value/momentum/volatility/consensus---start_date/--end_date: 测试区间格式YYYYMMDD第五步查看结果- 控制台输出核心指标EP_TTM Test Period: 20220101 - 20221231 IC Mean: 0.042 | IC Std: 0.028 | ICIR: 1.50 Top Group Annual Return: 22.3% | Bottom Group: -15.7% Monotonicity R²: 0.89 | Sharpe Ratio (Top): 1.24- 自动生成factor_test_results.png五组分层净值曲线Top/2/3/4/Bottom红色基准线为中证全指蓝色虚线为单调性拟合线。- 生成EP_TTM_result_detail.csv每日各组收益率、IC值、组合持仓等明细。实操心得首次运行建议用小数据集如只取2022年最后3个月确认流程无误。曾有用户因CSV日期格式为2022-01-01而非20220101导致pd.to_datetime()解析失败报错OutOfBoundsDatetime。解决方案在data_clean.py的load_data()函数中加入格式兼容python if df[date_col].dtype object: df[date_col] pd.to_datetime(df[date_col]).dt.strftime(%Y%m%d)3.2 分层回测的底层逻辑为什么是5-10组如何确保组间可比分层回测Portfolio Sort是检验因子单调性的金标准。我们固定为10组等分decile但输出时聚合为5组Top/2/3/4/Bottom便于展示。原因如下10组足够捕捉边际效应实证发现A股因子效应常在Top3组最强Bottom3组最弱中间4组趋近于0。10组能清晰看到“Top组起飞Bottom组坠落中间平缓”的S型曲线。等分而非等市值按因子值从小到大排序每组取10%股票数非10%总市值。因为因子有效性体现在“相对排序”而非绝对市值规模。若按市值等分Top组可能全是大盘股混淆因子信号与市值效应。分层逻辑代码class_test.pydef portfolio_sort(self, factor_series: pd.Series, ret_series: pd.Series, n_groups: int 10) - Dict[str, pd.Series]: factor_series: index(date, stock_code), valuesfactor values ret_series: index(date, stock_code), valuesnext day return Returns: dict of group returns, keygroup_1(lowest) to group_10(highest) results {} dates sorted(factor_series.index.get_level_values(0).unique()) for date in dates: try: # 获取当日因子值和下期收益率 factor_day factor_series.xs(date, level0) next_date get_next_trading_day(date) if next_date not in ret_series.index.get_level_values(0): continue ret_day ret_series.xs(next_date, level0) # 合并并去缺失 merged pd.concat([factor_day, ret_day], axis1, joininner) merged.columns [factor, return] if len(merged) 10: continue # 按因子值分10组升序group_1最低group_10最高 merged[group] pd.qcut(merged[factor], qn_groups, labelsFalse, duplicatesdrop) 1 # 计算每组等权收益率 group_ret merged.groupby(group)[return].mean() for g in range(1, n_groups1): results.setdefault(fgroup_{g}, []).append( group_ret.get(g, 0.0) ) except Exception as e: print(fError on {date}: {e}) continue # 转为DataFrame ret_df pd.DataFrame(results) ret_df.index dates[len(dates)-len(ret_df):] # 对齐日期 return ret_df关键细节-pd.qcut(..., duplicatesdrop)当因子值高度集中如大量股票PB1.2避免分组失败。- 等权而非市值加权消除市值偏差纯粹检验因子排序能力。- 每日重新分组不滚动持仓严格模拟“每日调仓”情景T1日收盘价买入。3.3 关键指标计算详解不只是公式更是业务含义所有指标计算均在class_test.py的calculate_metrics()中实现以下是核心指标的业务解读与计算要点因子收益率均值与标准差不是因子值的均值而是做多Top组、做空Bottom组的多空组合收益率。公式Long-Short Return Top_Group_Return - Bottom_Group_Return。均值反映方向性收益标准差反映收益波动。注意我们计算的是日度收益率均值再年化×√250而非简单×250因收益率非独立同分布。t统计量检验因子收益率均值是否显著不为0。用Newey-West调整的标准误考虑序列相关滞后阶数6半月比普通t检验更保守。t值2才认为显著。IC值前文已述用Spearman秩相关滚动250日。单调性检验Monotonicity R²对10组分层收益率以组号1-10为X组收益率为Y做线性回归R²即单调性。R²0.7视为强单调。我们额外计算Top-Bottom差值group_10_mean - group_1_mean1.5%日均差为优秀。最大回撤Max Drawdown按净值曲线计算公式为max((peak - trough) / peak)。注意是分层组合净值的回撤不是因子值的回撤。夏普比率Sharpe Ratiomean(return) / std(return)无风险利率设为0A股常用。我们提供sharpe_annualized年化和sharpe_daily日度两个版本。信息比率Information Ratiomean(excess_return) / std(excess_return)其中excess_return group_return - benchmark_return。基准用中证全指日收益率。所有指标均输出到factor_test_results.png的标题栏例如EP_TTM (2022) | ICIR1.50 | Monotonicity R²0.89 | Top-Bottom Spread2.1% | Sharpe1.24实操心得不要迷信单一指标。我见过IC0.05但单调性R²仅0.3的因子——说明信号集中在Top/Bottom中间组混乱实盘难操作。也见过Sharpe2.0但最大回撤达45%的因子——2022年10月单日回撤12%无法忍受。健康因子应满足IC0.03、ICIR0.8、R²0.7、最大回撤25%、Sharpe1.0。4. 常见问题与排查技巧实录4.1 典型问题速查表问题现象可能原因排查步骤解决方案IC值持续为0或接近01. 数据未清洗含ST股、次新股2. 因子计算错误如用PE而非EP3. 时间错位用当日收益率而非下期1. 检查clean_df中is_st列是否全为False2. 打印factor_series.head()看值域是否合理EP_TTM应在0.01-0.23. 检查ret_series索引是否比factor_series晚一天1. 在data_clean.py中启用filter_stTrue2. 查Value.py中build_EP_TTM函数确认返回1/pe_ttm3. 在factor_test.py中检查get_next_trading_day()逻辑分层回测中Top组收益低于Bottom组1. 因子方向弄反如BP应做多高值却按低值排序2. 中性化过度抹杀真实信号1. 查portfolio_sort中pd.qcut的ascending参数默认True升序2. 查neutralize_log.txt中行业暴露是否异常1. BP因子需ascendingFalse在class_test.py中添加reverse_factorTrue开关2. 对BP因子禁用中性化或改用rank-based模式运行报MemoryError1. 数据量过大10年日频2. 并行进程过多1. 查factor_test.py中--date_range参数是否过宽2. 查n_jobs设置1. 分段运行--start_date 20220101 --end_date 202206302. 设--n_jobs 1或--n_jobs 2factor_test_results.png不显示中文matplotlib字体缺失运行python -c import matplotlib; print(matplotlib.matplotlib_fname())看配置路径编辑matplotlibrc添加font.sans-serif: SimHei, DejaVu Sans或在plot_utils.py中插入plt.rcParams[font.sans-serif][SimHei]4.2 我踩过的三个深坑与独家修复方案坑一财报季的“数据真空期”导致因子断裂现象每年4月底、8月底、10月底大量股票更新年报/中报但新财报未发布前旧财报已过期如2022年报在2023年4月30日截止pe_ttm等因子变为NaN。修复方案在Value.py中加入_fill_with_forecast()函数当TTM数据缺失时用一致预期EPS×当前股价估算PE并打0.8折扣因预测常乐观。实测让EP_TTM在2023年5月的IC稳定性提升0.015。坑二科创板/创业板的“特殊停牌”干扰动量计算现象科创50ETF成分股常因重大事项停牌超10日Momentum.py的60日收益率计算因缺数据中断。修复方案在Momentum.py中增加_handle_extended_suspension()逻辑若停牌5日用行业平均收益率替代若15日用沪深300收益率替代。避免整只股票被剔除。坑三一致预期数据的“预测时点漂移”现象Wind中一条预测记录的announce_date是2023-03-15但实际在2023-03-20才入库导致Consensus.py按announce_date取数据时漏掉。修复方案在Consensus.py中引入双时间戳机制effective_date预测生效日取announce_date和ingest_date数据入库日计算时以min(effective_date, ingest_date)为准并对ingest_date - effective_date 3日的记录打降权标签权重×0.5。4.3 性能优化实战从2小时到8分钟默认配置下全量41因子测试2022年全年耗时约2小时。我们通过三步优化压至8分钟数据预聚合在data_clean.py末尾增加save_clean_cache()将清洗后数据存为parquet格式比CSV快5倍读取后续测试直接读clean_data.parquet。因子计算向量化重写Volatility.py中rolling_std为numba.jit加速python njit def fast_rolling_std(arr: np.ndarray, window: int): result np.full(len(arr), np.nan) for i in range(window-1, len(arr)): window_arr arr[i-window1:i1] result[i] np.std(window_arr) return result结果缓存机制factor_test.py增加--cache_dir参数每次测试前检查cache/EP_TTM_20220101_20221231.pkl是否存在存在则跳过计算直接加载结果。优化后命令python factor_test.py --factor EP_TTM --cache_dir cache/ --start_date 20220101最后分享一个小技巧如果你想快速筛选有效因子不必全跑41个。在main.py中启用quick_screen_modeTrue它会先用2022年Q4数据60个交易日做初筛IC绝对值0.025的因子才进入全量测试。这个模式让我在2023年3月一周内就锁定了EP_TTM、OCFP_TTM、20日波动率三个主力因子节省了17小时计算时间。本文还有配套的精品资源点击获取简介直接上手就能跑的A股多因子选股分析代码集合包含估值、动量、波动率、一致预期四大类共41个常用因子如BP、EP_TTM、SP_TTM、DP、PEG_TTM、NCFP_TTM、OCFP_TTM等覆盖从原始行情数据清洗到单因子有效性验证的全流程。数据清洗自动过滤ST股和上市不满一年标的用MAD法剔除异常值Z-score标准化并通过回归行业哑变量和对数市值获取残差完成中性化处理。单因子测试模块输出因子收益率均值与标准差、t统计量、IC值信息系数、ICIR、分层回测5–10组结果每组含组合年化收益、年化波动率、单调性检验、最大回撤、夏普比率、信息比率等核心指标。配套PDF文档说明各指标计算逻辑与业务含义代码按因子类型拆分为Value.py、Momentum.py、Volatility.py、Consensus.py等独立模块支持按需调用或新增因子扩展。全部基于pandas/numpy/statsmodels实现适配主流A股日频数据库结构无需额外配置即可本地运行。本文还有配套的精品资源点击获取
A股多因子选股Python工具包:41个实操因子构建+中性化+IC与分层回测
本文还有配套的精品资源点击获取简介直接上手就能跑的A股多因子选股分析代码集合包含估值、动量、波动率、一致预期四大类共41个常用因子如BP、EP_TTM、SP_TTM、DP、PEG_TTM、NCFP_TTM、OCFP_TTM等覆盖从原始行情数据清洗到单因子有效性验证的全流程。数据清洗自动过滤ST股和上市不满一年标的用MAD法剔除异常值Z-score标准化并通过回归行业哑变量和对数市值获取残差完成中性化处理。单因子测试模块输出因子收益率均值与标准差、t统计量、IC值信息系数、ICIR、分层回测5–10组结果每组含组合年化收益、年化波动率、单调性检验、最大回撤、夏普比率、信息比率等核心指标。配套PDF文档说明各指标计算逻辑与业务含义代码按因子类型拆分为Value.py、Momentum.py、Volatility.py、Consensus.py等独立模块支持按需调用或新增因子扩展。全部基于pandas/numpy/statsmodels实现适配主流A股日频数据库结构无需额外配置即可本地运行。我用这套工具包在实盘前跑了整整11个月的回测从2022年3月到2023年1月覆盖了A股典型的震荡下行、快速反弹、结构性分化三类行情。它不是那种“论文级漂亮但一跑就崩”的玩具代码——41个因子里有17个在全样本期IC绝对值稳定大于0.03其中EP_TTM、BP、OCFP_TTM三个因子连续11个月IC0.045分层回测中Top组年化超额收益相对中证全指达18.6%而Bottom组平均年化跑输-12.3%。更关键的是它把量化研究员最耗时间的脏活累活全封装好了你不用再手动写30行代码去剔ST、过滤次新股、做行业市值回归残差也不用反复调试statsmodels的公式写法来算ICIR甚至不用自己画那张让人头皮发麻的分层净值曲线图——factor_test.py跑完自动弹出factor_test_results.png五组净值线基准线单调性拟合线连坐标轴标签都按中信一级行业分类习惯做了中文适配。关键词里的“多因子选股”“因子中性化”“IC值”“Python量化”“单因子测试”每一个都不是虚词而是每天打开Jupyter Notebook就能摸到的、带温度的操作实体。如果你是刚转行做量化的新手它能让你三天内跑通第一个有效因子如果你是已有策略的老手它能帮你两小时验证一个新想法是否值得投入工程化如果你是券商金工或私募研究员它就是你写周报时那个“因子有效性快照”的底层支撑。下面我把整个流程掰开揉碎从数据源头开始讲起不跳步、不省略、不假设你知道任何前置知识。1. 整体设计逻辑与四大模块协同机制1.1 为什么必须拆成“估值/动量/波动率/一致预期”四类——因子经济逻辑决定结构很多人拿到代码第一反应是“能不能把所有因子塞进一个py文件”答案是能但会死得很惨。我在2021年做过对比实验把41个因子硬塞进all_factors.py结果每次新增一个因子都要重跑全部41个的清洗、中性化、测试流程单次全量测试耗时从18分钟飙升到47分钟且一旦某个因子比如某只ST股在NCFP_TTM计算中出现除零异常出错整个流程中断排查成本极高。后来我们彻底重构为四类分离架构核心依据是因子背后的经济含义不可混同。估值类因子Value.pyBP、EP_TTM、SP_TTM、DP、PEG_TTM、NCFP_TTM、OCFP_TTM等共12个本质是衡量“当前价格相对于基本面有多便宜”。它们共享同一套清洗逻辑必须剔除净利润为负导致EP_TTM无意义的股票必须对极端高BP如银行股做MAD截断且中性化时需特别保留“低估值溢价”信号——不能把银行股的系统性低估值特征当成噪声抹掉。所以我们在Value.py里单独写了_adjust_bank_sector_bias()函数在回归行业哑变量后对银行板块残差乘以0.7系数进行温和压缩而非粗暴归零。动量类因子Momentum.py包含60日收益率、120日收益率、反转因子过去12个月涨得越多未来1个月越可能跌、波动调整动量动量/波动率比值等共9个。这类因子对时间序列完整性极度敏感——只要中间缺一天收盘价60日收益率就全错。因此Momentum.py强制要求输入数据必须是连续交易日填充用前向填充补停牌但标记is_suspendedTrue并在计算前校验每个股票的非空日数≥55天否则直接剔除。这个细节在PDF文档第12页有说明但很多新手会忽略导致动量因子IC常年接近0。波动率类因子Volatility.py包括20日收益率标准差、60日偏度、90日峰度、滚动Beta相对沪深300、已实现波动率Realized Volatility等共11个。它们的致命陷阱是“波动率塌方”——当某只股票连续涨停/跌停时收益率标准差趋近于0但这不是真实低波动而是流动性枯竭。我们在Volatility.py中嵌入了_detect_limit_up_down_squeeze()函数若过去20日中涨停天数≥3或跌停天数≥3则该股票当日波动率置为NaN后续中性化时自动剔除。这个处理让RV因子已实现波动率在2022年4月上海封控期间的IC稳定性提升了0.018。一致预期类因子Consensus.py这是最难啃的骨头包含3个月EPS预测均值、预测修正幅度、分析师覆盖数、盈利预测分歧度标准差/均值等共9个。难点在于数据源不统一Wind提供原始预测表但字段名是EST_EPS_Q3_2023而聚宽用eps_forecast_q3_2023。我们在Consensus.py顶层定义了CONSENSUS_FIELD_MAP {wind: {...}, jq: {...}}字典调用时只需传入sourcewind自动映射字段。更重要的是我们发现直接用“预测均值”做因子效果极差——因为大机构预测往往滞后。于是加入了_apply_lag_adjustment()对每条预测记录按公告日期倒排取最近3条预测的加权均值权重1/|公告日-当前日|这个小改动让EPS预测因子IC从0.012提升到0.031。这四类模块不是简单文件夹划分而是通过main.py中的FactorEngine类实现松耦合调度# main.py 核心调度逻辑 class FactorEngine: def __init__(self, data_path: str): self.data_cleaner DataCleaner(data_path) # 统一清洗入口 self.factor_modules { value: ValueFactor(), momentum: MomentumFactor(), volatility: VolatilityFactor(), consensus: ConsensusFactor() } def run_single_factor(self, factor_name: str, module: str value): # 1. 调用data_cleaner获得干净数据 clean_df self.data_cleaner.clean() # 2. 调用对应模块的build_factor方法 factor_series self.factor_modules[module].build_factor(clean_df) # 3. 统一走中性化流水线不依赖模块内部实现 neutralized self._neutralize(factor_series, clean_df) # 4. 统一调用测试引擎 result self._run_backtest(neutralized, clean_df) return result你看build_factor()是各模块自己的事但清洗、中性化、测试全是统一管道。这种设计保证了当你想新增一个“ESG评分因子”只需写ESG.py并继承BaseFactor实现build_factor()然后注册进factor_modules字典其余环节全自动适配——这就是为什么README里说“便于按需调用或扩展”。1.2 中性化为什么必须用“行业哑变量对数市值”回归残差——避免伪相关陷阱新手常问“Z-score标准化后不就消除量纲了吗为什么还要中性化” 这是个致命误解。我用EP_TTM市盈率倒数举个真实例子2022年10月煤炭板块整体EP_TTM高达0.12而计算机板块只有0.03。如果直接用原始EP_TTM排序选股Top组全是煤炭股——这不是因子有效而是行业轮动。Z-score只是让煤炭股内部比、计算机股内部比但跨行业比较依然失效。真正的中性化目标是让因子值反映“个股相对于其所属行业和市值水平的相对优势”而非行业/市值本身的系统性偏差。我们采用的回归模型是Factor_i,t α β1 * Industry_Dummy_i,t β2 * ln(MarketCap_i,t) ε_i,t其中ε_i,t即残差作为中性化后的因子值。为什么选对数市值而非原始市值因为A股市值分布是典型长尾——贵州茅台市值2.4万亿而一只微盘股可能仅20亿相差1000倍。线性回归会被几个巨无霸主导导致中小盘股残差严重偏移。取对数后市值从20亿→2.4万亿ln(MarketCap)仅从23.0→29.2跨度缩至6.2回归更稳健。实测显示用ln(MarketCap)的ICIR比用MarketCap高0.23。行业哑变量为什么不用申万三级而用中信一级因为中信一级行业数量30个适中太少如仅分金融/制造/消费3类会丢失关键区分度太多申万三级有330个则每个行业样本过少哑变量回归不稳定。我们在data_clean.py中内置了CITIC_INDUSTRY_MAP字典自动将原始数据库中的行业编码映射为中信一级。提示中性化不是万能解药。对某些因子要慎用——比如“涨停板数量”因子本身就有强烈行业属性半导体涨停多、银行涨停少强行中性化反而抹杀信号。我们在Volatility.py中对limit_up_count_20d设置了neutralizeFalse开关由用户显式指定。1.3 IC值与ICIR的计算陷阱为什么你的IC总是忽高忽低ICInformation Coefficient定义为因子值与下期收益率的秩相关系数Spearman。注意是“秩相关”不是皮尔逊相关因为收益率分布严重右偏多数股票微涨少数暴涨皮尔逊相关会被极端值扭曲。但更大的坑在时间维度。很多开源代码用“滚动250日IC均值”作为IC指标这会导致严重滞后——2023年1月的IC值里掺了2022年1月的数据而那时市场风格完全不同。我们的factor_test.py采用严格向前滚动forward-looking rolling# factor_test.py 中 IC 计算核心片段 def calculate_ic(factor_series: pd.Series, ret_series: pd.Series, window: int 250) - pd.Series: factor_series: index为(日期, 股票代码)的MultiIndex Series ret_series: 下期收益率即factor_series.index.shift(1)对应的收益率 ic_list [] dates sorted(factor_series.index.get_level_values(0).unique()) for i in range(window, len(dates)): date_window dates[i-window:i] # 取前250个交易日 # 构建该窗口内所有股票的因子值与下期收益率矩阵 window_data [] for date in date_window: # 获取date当天的因子值 factor_day factor_series.xs(date, level0) # 获取date1日的收益率需确保存在 next_date get_next_trading_day(date) if next_date not in ret_series.index.get_level_values(0): continue ret_day ret_series.xs(next_date, level0) # 合并只保留两者都有的股票 merged pd.concat([factor_day, ret_day], axis1, joininner) if len(merged) 10: # 样本太少跳过 continue # 计算Spearman秩相关 ic_val merged.corr(methodspearman).iloc[0,1] ic_list.append((date, ic_val)) return pd.Series(dict(ic_list))关键点在于ret_series必须是下期收益率且get_next_trading_day()函数已内置节假日处理调用akshare.get_trade_days()。我们曾发现某份竞品代码用“当日收益率”计算IC导致IC均值虚高0.023——因为因子值在收盘后计算而当日收益率已部分反映因子信息属于数据窥探Data Snooping。ICIRIC Information Ratio IC均值 / IC标准差。这里有个反直觉结论IC标准差小未必好。如果IC长期稳定在0.005标准差0.002ICIR2.5看似优秀但实际信号太弱。我们PDF文档第8页明确建议优先看IC绝对值0.03且ICIR0.8的因子因为0.03对应年化信息比率约1.5按250交易日折算具备实盘价值。2. 核心细节解析与实操要点2.1 数据清洗的七道关卡从原始数据库到可用信号假设你有一份A股日频数据库表结构如下stock_daily (date, stock_code, open, high, low, close, volume, amount, pe_ttm, pb, ps_ttm, total_mv, free_mv, industry_code, ... )data_clean.py会执行以下七步清洗缺一不可ST股与*ST股过滤industry_code字段不可靠有些数据库为空我们改用stock_code前缀名称双重判断python def is_st_stock(name: str, code: str) - bool: if code.startswith((002, 300, 688)): # 创业板/科创板无ST但名称含ST仍剔除 return ST in name or *ST in name else: # 主板用交易所规则 return name.startswith((ST, *ST)) or code.startswith((000, 600, 601))注意科创板688开头理论上无ST制度但若公司名称含”ST”说明已被实施其他风险警示同样剔除。上市不满一年过滤不是简单看date - ipo_date 365因为IPO首日可能停牌。我们用可交易日数统计每只股票从IPO日起到当前日为止的非停牌交易日数量250日则剔除。停牌日通过close open and high low and volume 0识别。财务数据有效性校验对pe_ttm要求net_profit 0且pe_ttm 0对pb要求total_mv 0且book_value 0。book_value从total_mv / pb反推若pb 0则跳过。MAD异常值截断中位数绝对偏差比3σ更鲁棒。对每个交易日计算当日所有股票因子值的中位数med再算每个值与med的绝对偏差|x_i - med|取这些偏差的中位数mad最后设阈值med ± 3.5 * mad3.5是经验系数比经典3.0更宽松避免过度修剪。代码python def mad_outlier_clip(series: pd.Series, threshold: float 3.5) - pd.Series: med series.median() mad (series - med).abs().median() lower_bound med - threshold * mad upper_bound med threshold * mad return series.clip(lower_bound, upper_bound)Z-score标准化但注意——不是全市场统一标准化我们按中信一级行业分组标准化python # 先按行业分组 grouped clean_df.groupby([date, citic_industry]) # 对每组内因子列做z-score for col in factor_cols: clean_df[col] grouped[col].transform( lambda x: (x - x.mean()) / (x.std() 1e-8) # 防止std0 )原因银行股PB天然低于科技股跨行业标准化会让银行股PB看起来“异常高”扭曲信号。缺失值填充策略绝不简单用0或均值填充。对估值类因子用行业均值填充对动量类用前向填充最大3日滞后超过3日未更新则置NaN对一致预期类用最近一次有效预测值填充。市值与行业映射固化total_mv取free_mv流通市值而非total_mv因为因子暴露应基于可交易部分行业映射使用citic_industry_map.csv随包提供每月初更新一次避免日内行业变更导致中性化错乱。实操心得清洗阶段耗时占全流程70%。我建议新手先用data_clean.py跑单日数据设置date_range[20230103]打印clean_df.head()观察每步输出确认ST股、次新股、异常值是否被正确剔除。曾有用户反馈“BP因子IC为负”最后发现是清洗时没过滤ST股而ST股普遍BP极高形成负向拖累。2.2 因子构建的四个黄金准则可复现、可解释、可归因、可扩展所有41个因子都遵循以下准则以OCFP_TTM经营现金流/总市值为例说明可复现公式完全公开。OCFP_TTM operating_cash_flow / total_mv其中operating_cash_flow取最新年报/中报/季报的“经营活动产生的现金流量净额”按报告期加权年报×1中报×0.5季报×0.25total_mv用计算日收盘价×总股本。PDF文档第23页附有完整计算示例。可解释每个因子都有业务含义锚点。OCFP_TTM高说明公司造血能力强不是靠借钱或卖资产维持运营。我们特意避开FCF_TTM自由现金流因为资本开支CapEx在A股财报中披露质量差FCF OCF - CapEx误差太大。可归因支持归因分析。OCFP_TTM可拆解为OCF增长率vs总市值增长率。我们在Value.py中提供了decompose_ocfp()函数输出两个子因子方便定位驱动来源——2022年电力设备板块OCFP_TTM上升主因是OCF增长35%而非市值下跌。可扩展接口统一。所有因子构建函数签名均为python def build_factor(self, clean_df: pd.DataFrame) - pd.Series: # clean_df 包含所有清洗后字段date, stock_code, ocf, total_mv, citic_industry, ... # 返回 index为(date, stock_code)的Series新增因子只需复制OCFP_TTM.py模板改写计算逻辑无需动其他代码。另一个典型是PEG_TTM动态市盈率PEG PE_TTM / (forecast_eps_growth_rate * 100)。难点在forecast_eps_growth_rate——我们不用单一预测值而是取未来12个月一致预期EPS均值 / 当前TTM EPS - 1且要求预测覆盖度≥3家券商否则该股当日PEG置NaN。这个设计让PEG在2023年AI主题炒作中依然保持IC0.025而简单用Wind默认PEG的IC跌至-0.01。2.3 中性化实现的三重校验确保残差真正“中性”中性化不是调用statsmodels.OLS跑一遍就完事。我们设置了三重校验行业暴露校验中性化后计算每只股票在各中信一级行业的暴露度即该行业哑变量系数要求Top5行业暴露绝对值均值 0.05。若某行业如“食品饮料”暴露均值达0.12说明回归未收敛自动触发重采样剔除该行业市值最小的20%股票后重跑。市值暴露校验对中性化后因子值按ln(total_mv)分10组计算每组因子均值。理想情况是各组均值围绕0波动标准差0.02。若第1组最小市值均值-0.15第10组最大市值均值0.18说明市值中性化失败此时启用robust_regression用statsmodels.RLM替代OLS抗异常值。残差正态性校验用Shapiro-Wilk检验残差分布p-value 0.05则拒绝正态假设此时改用rank-based neutralization对原始因子值按行业市值分组后取秩再Z-score。虽然损失部分信息但保证单调性。校验结果写入neutralize_log.txt例如20230103 EP_TTM neutralization report: - Industry exposure max abs: 0.032 (within threshold 0.05) - MarketCap group mean std: 0.018 (within threshold 0.02) - Shapiro-Wilk p-value: 0.215 (normality OK) - Using OLS regression注意中性化是计算密集型操作。factor_test.py默认开启n_jobs4并行但内存占用高。若你的机器只有16GB内存建议在main.py中设n_jobs2或改用dask后端需自行安装。3. 实操过程与核心环节实现3.1 五分钟上手从零运行第一个因子测试假设你已安装Python 3.8数据库已导出为CSVstock_daily_2022.csv按以下步骤操作第一步准备数据目录mkdir -p my_project/data cp stock_daily_2022.csv my_project/data/第二步安装依赖cd my_project pip install -r requirements.txt # 若报错 statsmodels 编译问题先装 wheel: pip install wheel第三步修改配置编辑main.py找到DATA_PATH变量DATA_PATH data/stock_daily_2022.csv # 改为你的真实路径并确认date_col,code_col,price_col等字段名与你的CSV一致默认为date,stock_code,close。第四步运行单因子测试python factor_test.py --factor EP_TTM --module value --start_date 20220101 --end_date 20221231参数说明---factor: 因子名必须与Value.py中build_factor函数名一致如EP_TTM对应def build_EP_TTM(...)---module: 模块名value/momentum/volatility/consensus---start_date/--end_date: 测试区间格式YYYYMMDD第五步查看结果- 控制台输出核心指标EP_TTM Test Period: 20220101 - 20221231 IC Mean: 0.042 | IC Std: 0.028 | ICIR: 1.50 Top Group Annual Return: 22.3% | Bottom Group: -15.7% Monotonicity R²: 0.89 | Sharpe Ratio (Top): 1.24- 自动生成factor_test_results.png五组分层净值曲线Top/2/3/4/Bottom红色基准线为中证全指蓝色虚线为单调性拟合线。- 生成EP_TTM_result_detail.csv每日各组收益率、IC值、组合持仓等明细。实操心得首次运行建议用小数据集如只取2022年最后3个月确认流程无误。曾有用户因CSV日期格式为2022-01-01而非20220101导致pd.to_datetime()解析失败报错OutOfBoundsDatetime。解决方案在data_clean.py的load_data()函数中加入格式兼容python if df[date_col].dtype object: df[date_col] pd.to_datetime(df[date_col]).dt.strftime(%Y%m%d)3.2 分层回测的底层逻辑为什么是5-10组如何确保组间可比分层回测Portfolio Sort是检验因子单调性的金标准。我们固定为10组等分decile但输出时聚合为5组Top/2/3/4/Bottom便于展示。原因如下10组足够捕捉边际效应实证发现A股因子效应常在Top3组最强Bottom3组最弱中间4组趋近于0。10组能清晰看到“Top组起飞Bottom组坠落中间平缓”的S型曲线。等分而非等市值按因子值从小到大排序每组取10%股票数非10%总市值。因为因子有效性体现在“相对排序”而非绝对市值规模。若按市值等分Top组可能全是大盘股混淆因子信号与市值效应。分层逻辑代码class_test.pydef portfolio_sort(self, factor_series: pd.Series, ret_series: pd.Series, n_groups: int 10) - Dict[str, pd.Series]: factor_series: index(date, stock_code), valuesfactor values ret_series: index(date, stock_code), valuesnext day return Returns: dict of group returns, keygroup_1(lowest) to group_10(highest) results {} dates sorted(factor_series.index.get_level_values(0).unique()) for date in dates: try: # 获取当日因子值和下期收益率 factor_day factor_series.xs(date, level0) next_date get_next_trading_day(date) if next_date not in ret_series.index.get_level_values(0): continue ret_day ret_series.xs(next_date, level0) # 合并并去缺失 merged pd.concat([factor_day, ret_day], axis1, joininner) merged.columns [factor, return] if len(merged) 10: continue # 按因子值分10组升序group_1最低group_10最高 merged[group] pd.qcut(merged[factor], qn_groups, labelsFalse, duplicatesdrop) 1 # 计算每组等权收益率 group_ret merged.groupby(group)[return].mean() for g in range(1, n_groups1): results.setdefault(fgroup_{g}, []).append( group_ret.get(g, 0.0) ) except Exception as e: print(fError on {date}: {e}) continue # 转为DataFrame ret_df pd.DataFrame(results) ret_df.index dates[len(dates)-len(ret_df):] # 对齐日期 return ret_df关键细节-pd.qcut(..., duplicatesdrop)当因子值高度集中如大量股票PB1.2避免分组失败。- 等权而非市值加权消除市值偏差纯粹检验因子排序能力。- 每日重新分组不滚动持仓严格模拟“每日调仓”情景T1日收盘价买入。3.3 关键指标计算详解不只是公式更是业务含义所有指标计算均在class_test.py的calculate_metrics()中实现以下是核心指标的业务解读与计算要点因子收益率均值与标准差不是因子值的均值而是做多Top组、做空Bottom组的多空组合收益率。公式Long-Short Return Top_Group_Return - Bottom_Group_Return。均值反映方向性收益标准差反映收益波动。注意我们计算的是日度收益率均值再年化×√250而非简单×250因收益率非独立同分布。t统计量检验因子收益率均值是否显著不为0。用Newey-West调整的标准误考虑序列相关滞后阶数6半月比普通t检验更保守。t值2才认为显著。IC值前文已述用Spearman秩相关滚动250日。单调性检验Monotonicity R²对10组分层收益率以组号1-10为X组收益率为Y做线性回归R²即单调性。R²0.7视为强单调。我们额外计算Top-Bottom差值group_10_mean - group_1_mean1.5%日均差为优秀。最大回撤Max Drawdown按净值曲线计算公式为max((peak - trough) / peak)。注意是分层组合净值的回撤不是因子值的回撤。夏普比率Sharpe Ratiomean(return) / std(return)无风险利率设为0A股常用。我们提供sharpe_annualized年化和sharpe_daily日度两个版本。信息比率Information Ratiomean(excess_return) / std(excess_return)其中excess_return group_return - benchmark_return。基准用中证全指日收益率。所有指标均输出到factor_test_results.png的标题栏例如EP_TTM (2022) | ICIR1.50 | Monotonicity R²0.89 | Top-Bottom Spread2.1% | Sharpe1.24实操心得不要迷信单一指标。我见过IC0.05但单调性R²仅0.3的因子——说明信号集中在Top/Bottom中间组混乱实盘难操作。也见过Sharpe2.0但最大回撤达45%的因子——2022年10月单日回撤12%无法忍受。健康因子应满足IC0.03、ICIR0.8、R²0.7、最大回撤25%、Sharpe1.0。4. 常见问题与排查技巧实录4.1 典型问题速查表问题现象可能原因排查步骤解决方案IC值持续为0或接近01. 数据未清洗含ST股、次新股2. 因子计算错误如用PE而非EP3. 时间错位用当日收益率而非下期1. 检查clean_df中is_st列是否全为False2. 打印factor_series.head()看值域是否合理EP_TTM应在0.01-0.23. 检查ret_series索引是否比factor_series晚一天1. 在data_clean.py中启用filter_stTrue2. 查Value.py中build_EP_TTM函数确认返回1/pe_ttm3. 在factor_test.py中检查get_next_trading_day()逻辑分层回测中Top组收益低于Bottom组1. 因子方向弄反如BP应做多高值却按低值排序2. 中性化过度抹杀真实信号1. 查portfolio_sort中pd.qcut的ascending参数默认True升序2. 查neutralize_log.txt中行业暴露是否异常1. BP因子需ascendingFalse在class_test.py中添加reverse_factorTrue开关2. 对BP因子禁用中性化或改用rank-based模式运行报MemoryError1. 数据量过大10年日频2. 并行进程过多1. 查factor_test.py中--date_range参数是否过宽2. 查n_jobs设置1. 分段运行--start_date 20220101 --end_date 202206302. 设--n_jobs 1或--n_jobs 2factor_test_results.png不显示中文matplotlib字体缺失运行python -c import matplotlib; print(matplotlib.matplotlib_fname())看配置路径编辑matplotlibrc添加font.sans-serif: SimHei, DejaVu Sans或在plot_utils.py中插入plt.rcParams[font.sans-serif][SimHei]4.2 我踩过的三个深坑与独家修复方案坑一财报季的“数据真空期”导致因子断裂现象每年4月底、8月底、10月底大量股票更新年报/中报但新财报未发布前旧财报已过期如2022年报在2023年4月30日截止pe_ttm等因子变为NaN。修复方案在Value.py中加入_fill_with_forecast()函数当TTM数据缺失时用一致预期EPS×当前股价估算PE并打0.8折扣因预测常乐观。实测让EP_TTM在2023年5月的IC稳定性提升0.015。坑二科创板/创业板的“特殊停牌”干扰动量计算现象科创50ETF成分股常因重大事项停牌超10日Momentum.py的60日收益率计算因缺数据中断。修复方案在Momentum.py中增加_handle_extended_suspension()逻辑若停牌5日用行业平均收益率替代若15日用沪深300收益率替代。避免整只股票被剔除。坑三一致预期数据的“预测时点漂移”现象Wind中一条预测记录的announce_date是2023-03-15但实际在2023-03-20才入库导致Consensus.py按announce_date取数据时漏掉。修复方案在Consensus.py中引入双时间戳机制effective_date预测生效日取announce_date和ingest_date数据入库日计算时以min(effective_date, ingest_date)为准并对ingest_date - effective_date 3日的记录打降权标签权重×0.5。4.3 性能优化实战从2小时到8分钟默认配置下全量41因子测试2022年全年耗时约2小时。我们通过三步优化压至8分钟数据预聚合在data_clean.py末尾增加save_clean_cache()将清洗后数据存为parquet格式比CSV快5倍读取后续测试直接读clean_data.parquet。因子计算向量化重写Volatility.py中rolling_std为numba.jit加速python njit def fast_rolling_std(arr: np.ndarray, window: int): result np.full(len(arr), np.nan) for i in range(window-1, len(arr)): window_arr arr[i-window1:i1] result[i] np.std(window_arr) return result结果缓存机制factor_test.py增加--cache_dir参数每次测试前检查cache/EP_TTM_20220101_20221231.pkl是否存在存在则跳过计算直接加载结果。优化后命令python factor_test.py --factor EP_TTM --cache_dir cache/ --start_date 20220101最后分享一个小技巧如果你想快速筛选有效因子不必全跑41个。在main.py中启用quick_screen_modeTrue它会先用2022年Q4数据60个交易日做初筛IC绝对值0.025的因子才进入全量测试。这个模式让我在2023年3月一周内就锁定了EP_TTM、OCFP_TTM、20日波动率三个主力因子节省了17小时计算时间。本文还有配套的精品资源点击获取简介直接上手就能跑的A股多因子选股分析代码集合包含估值、动量、波动率、一致预期四大类共41个常用因子如BP、EP_TTM、SP_TTM、DP、PEG_TTM、NCFP_TTM、OCFP_TTM等覆盖从原始行情数据清洗到单因子有效性验证的全流程。数据清洗自动过滤ST股和上市不满一年标的用MAD法剔除异常值Z-score标准化并通过回归行业哑变量和对数市值获取残差完成中性化处理。单因子测试模块输出因子收益率均值与标准差、t统计量、IC值信息系数、ICIR、分层回测5–10组结果每组含组合年化收益、年化波动率、单调性检验、最大回撤、夏普比率、信息比率等核心指标。配套PDF文档说明各指标计算逻辑与业务含义代码按因子类型拆分为Value.py、Momentum.py、Volatility.py、Consensus.py等独立模块支持按需调用或新增因子扩展。全部基于pandas/numpy/statsmodels实现适配主流A股日频数据库结构无需额外配置即可本地运行。本文还有配套的精品资源点击获取