ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

量化因子研究收官指南:从数据清洗到因子库的完整链路

量化因子研究收官指南:从数据清洗到因子库的完整链路 “365天量化金融90因子阶段收官”看到这个标题的你大概率和我一样已经吭哧吭哧写了很久的因子。单因子一个个回测出来都像是有 alpha组合起来却总是对不上账Notebook 里跑得出来的结果换个时间窗口就变样研究目录里七八个factor_v5_final_v2.xlsx过两个月自己都不想打开。这是量化因子研究的典型尴尬期。如果你正好卡在这个阶段这篇文章值得看完。我想借“第90篇”这个节点把因子阶段真正要收尾的事情讲清楚因子阶段收官的标志不是因子数量多而是任意一个因子都能在 30 分钟内从原始行情数据完整复现、完成检验、登记入库并被上层回测系统稳定调用。换句话说这轮工作的核心成果不是那几十个因子而是从数据流到因子库的整条链路。本文会从数据清洗、因子计算、单因子检验、方差分析到因子库表结构设计全过程拆解一遍并给出可直接落地的 Python 代码和工程建议。1. 因子阶段收官真正要收的是什么很多人的因子研究流程是这样的从某个交易软件或者公开数据源下载行情在 Notebook 里写一个公式算指标用df[factor]算完画一张净值图看得顺眼就留下不顺眼就换个参数继续试。这套流程在因子数量只有几个的时候没有问题一旦因子数量超过 20 个问题就会集中爆发。第一个问题是不可复现。当时的代码依赖某个没写进 requirements 的库版本或者某段数据是手动删除过异常行之后才计算的过两周再跑结果对不上。第二个问题是不可对比。每个因子的回测区间不同、股票池不同、处理方式不同你根本没法在同一个标准下比较它们的优劣。第三个问题是不可使用。研究阶段算出来的是一个 DataFrame但真正要上策略回测时系统需要的是一个按日期和股票对齐的因子面板这个面板的生成、更新、存储、读取完全没有工程化。所以“因子阶段收官”这四个字的重点不在“收官”而在把之前散落在 Notebook 里的探索性代码整理成一条从数据到因子的工业化流水线。判断标准很简单删掉你本地的所有 ipynb 缓存只保留一条命令能让整个因子库从原始数据重新构建出来并且生成一份完整的因子检验报告。这篇文章我会沿着“数据流 → 因子计算 → 因子检验 → 因子库”的顺序来讲。整个过程里面最容易踩的坑往往不是模型有多复杂而是数据对齐、因子标准化、幸存者偏差、前视偏差这些基础问题。2. 从数据流到因子库一条完整的量化链路先给这一整条链路画一个总览后续所有操作都在这个框架里展开。链路大致分为四层层次核心职责典型产物主要风险数据层原始行情、财务、行业数据的采集与清洗分钟/日线 bar 数据、复权因子、财报数据复权错误、停牌缺失、幸存者偏差因子计算层把因子公式转成可复用、可批量执行的代码因子值面板date × stock前视偏差、算错会计日、因子实现不一致因子检验层单因子有效性检验、分组收益检验、显著性检验IC/IR 序列、分层回测结果、F 检验 p 值区间选择偏差、多重检验因子库层因子数据与元信息的统一存储和调用因子明细表、因子元数据表、因子快照版本数据版本混乱、字段口径不一致数据层是地基。很多人觉得“拿一份数据就能算因子”实际上你在思考一个因子公式之前必须先回答几个问题股票池是什么是全部 A 股还是剔除 ST 和次新股停牌日怎么处理除权除息是用前复权还是后复权如果这些口径不统一后面所有因子检验都会失真。因子计算层解决的是“口径一致”的问题。同样的动量公式在通达信里喊出来是一个值在聚宽里跑出来是另一个值自己用 pandas 写又是第三个值。这不是因为哪个对哪个错而是对涨跌停、取价方式、收益计算区间的定义不同。因子库必须有唯一确定的计算定义并尽量用统一的底层函数库去实现。因子检验层回答“这个因子有没有用”。我常用的验证手段至少有三种Rank IC 与 IR 检验、分层回测、单因子方差分析 F 检验。三者各有侧重不应互相替代。因子库层把前面所有结果沉淀下来。数据要入库程序要入库公式和参数也要入库。很多团队因子代码在研究员手里数据表在数据库里业务含义只有在 EXCEL 备注里这种状态很难支撑后续的策略研发。3. 数据层把原始行情变成干净的样本面板因子计算的第一步是准备一个干净的“样本面板”。我一直建议先做一张标准化的日线行情宽表而不是每个因子各取所需。这张宽表的每一行表示“某只股票在某一天的行情快照”字段建议包括trade_date 交易日期 stock_code 股票代码 close 收盘价 open 开盘价 high 最高价 low 最低价 volume 成交量 amount 成交额 adjust_factor 复权因子 is_trading 是否正常交易在实际工程中这张表通常来自聚宽、米筐、Tushare 或者恒生、Wind 等数据源但最终都要落成本地数据库表。落库之后第一件事不是算因子而是做数据质量检查。数据清洗至少有四个必做动作。动作一剔除或标记停牌日和无交易记录。停牌时期的收盘价往往是上一交易日的收盘价延续如果直接参与收益计算会造成大量零收益样本干扰因子检验结果。动作二复权处理。因子计算建议统一使用“后复权价格”计算收益率避免前复权价格导致历史区间收益被压缩的问题。除非你的因子本身就是基于未复权价格设计比如价格阈值类因子否则一律用后复权。动作三股票池过滤。静态股票池通常建议在每个调仓日过滤掉 ST、退市风险股、上市不足 60 天的次新股并且要避免使用未来信息去过滤。这里很容易犯的错误是用今天的 ST 状态去剔除昨天历史数据中的股票这在回测里是合法的因为调仓决策发生时你已经知道 ST 状态但在做因子研究时如果你用整段时间的最终股票池去筛选会导致幸存者偏差。动作四收益率计算口径统一。建议统一成pct_change或者close.shift(-1) / close - 1并且要明确一个因子到底在描述“今天截面上的属性”还是“未来收益的预测”。我的习惯是因子值用 T 日收盘后可得信息计算预测目标用 T1 到 TN 的收益保证不会因为计算时点错误造成前视偏差。下面是一段通用的数据清洗与收益对齐代码可以作为因子计算的公共函数库基础# 文件路径quant_common/market_data_utils.py import pandas as pd import numpy as np def load_clean_daily_bars(raw_df: pd.DataFrame) - pd.DataFrame: 将原始行情 DataFrame 清洗为标准日线宽表。 参数 ---- raw_df : DataFrame 至少包含 trade_date, stock_code, close, volume, amount 列。 返回 ---- DataFrame : 按 (trade_date, stock_code) 排序的宽表包含 adjusted_close 列。 df raw_df.copy() df[trade_date] pd.to_datetime(df[trade_date]) df df.sort_values([stock_code, trade_date]).reset_index(dropTrue) # 简单复权这里用 adjust_factor 做后复权。 # 如果数据源没有复权因子可先用 close * cumprod(除权除息比例) 近似。 # 注意不要用前复权价直接跨长时间段计算因子。 if adjust_factor in df.columns: df[adjusted_close] df[close] * df[adjust_factor] else: df[adjusted_close] df[close] # 剔除成交量或收盘价为空的行情记录 df df.replace([np.inf, -np.inf], np.nan) df df.dropna(subset[close, volume]) return df def calc_future_return(df: pd.DataFrame, forward_days: int 5) - pd.DataFrame: 计算因子预测目标未来 N 日收益。 注意这里使用 adjusted_close 计算收益率且 shift 是在股票维度内进行的 绝不能在整个 DataFrame 上直接 shift否则会跨界取到别的股票数据。 df df.sort_values([stock_code, trade_date]).reset_index(dropTrue) # 股票内下一期 close df[future_close] df.groupby(stock_code)[adjusted_close].shift(-forward_days) df df.dropna(subset[future_close]) # 未来 N 日收益 future_close / 当前 close - 1 # 这里未来收益必须用未来价格计算因子值本身不能用未来信息。 df[target_return] df[future_close] / df[adjusted_close] - 1.0 return df这段代码的边界要澄清一下shift(-forward_days)使用的是未来收盘价这只能在构造训练标签时使用绝不能在因子值计算时使用。很多刚接触量化的人会在这里犯错算动量因子时用到了未来数据导致回测曲线异常漂亮实盘直接失效。4. 因子计算层因子如何从公式变成可复现代码数据干净之后下一步就是把因子公式变成可复用代码。这里最忌讳的是每个因子一个孤立函数各算各的内部逻辑重复且不一致。更好的做法是定义一套“因子算子”的公共层。比如动量类因子里你可能会用到“过去 N 日收益率”“过去 N 日收益率的波动率”“过去 N 日收益率与成交量的相关性”。这些算子都应该抽成公共函数然后因子定义只负责组合这些算子。我建议的目录结构如下quant_research/ ├── data/ │ ├── raw/ # 原始数据缓存 │ └── processed/ # 清洗后的宽表 ├── factors/ │ ├── operators.py # 公共算子 │ ├── factor_defs.py # 因子定义与注册 │ └── factor_pipeline.py# 批量计算入口 ├── tests/ │ └── test_factors.py # 因子计算的正确性测试 └── metrics/ ├── ic_analysis.py # IC/IR 分析 ├── group_backtest.py # 分层回测 └── anova_test.py # 单因子方差分析公共算子的意义在于你只需要保证动量、波动率、相关性、分位数这些基础算子的正确性后续所有因子都建立在这套算子上不容易出现口径漂移。下面是一段动量类因子代码示例体现了“公共算子 因子注册”的思路# 文件路径quant_research/factors/operators.py import pandas as pd import numpy as np def rolling_cum_return(close: pd.Series, window: int) - pd.Series: 过去 window 个交易日的累计收益率使用复权价计算。 return close / close.shift(window) - 1.0 def rolling_volatility(close: pd.Series, window: int) - pd.Series: 过去 window 个交易日的对数收益率标准差。 log_ret np.log(close).diff() return log_ret.rolling(window).std() def rolling_corr(left: pd.Series, right: pd.Series, window: int) - pd.Series: 两个序列的滚动相关系数。 return left.rolling(window).corr(right)# 文件路径quant_research/factors/factor_defs.py from operators import rolling_cum_return, rolling_volatility def momentum_20d(df: pd.DataFrame) - pd.Series: 20 日动量因子过去 20 个交易日累计收益率。 return df.groupby(stock_code)[adjusted_close].apply( lambda s: rolling_cum_return(s, 20) ).reset_index(level0, dropTrue) def volatility_20d(df: pd.DataFrame) - pd.Series: 20 日波动率因子过去 20 个交易日对数收益率的滚动标准差。 return df.groupby(stock_code)[adjusted_close].apply( lambda s: rolling_volatility(s, 20) ).reset_index(level0, dropTrue) # 因子注册表新增因子只需要在 dict 里加一行 FACTOR_REGISTRY { momentum_20d: momentum_20d, volatility_20d: volatility_20d, }因子注册表是因子库的雏形。后续如果你想系统化管理所有因子甚至可以在注册表里加入因子类别、作者、创建日期、依赖数据版本等元信息而不是只存一个函数名。5. 因子检验层如何判断一个因子真的有效因子算出来之后最重要的一步就是检验。我一直强调单因子的“回测收益曲线好看”是最不可靠的评价标准因为区间选择、参数遍历、幸存者偏差都可能制造幻象。更稳的检验组合是Rank IC 检验、分层回测、单因子方差分析 F 检验。5.1 因子与未来收益的 Rank IC 计算ICInformation Coefficient衡量因子值与未来收益之间的相关性。常用的有三种Pearson IC、Spearman Rank IC、以及经过行业市值中性化后的残差 IC。我个人最常用 Rank IC因为它在截面上对极端值不那么敏感。计算步骤是在每一个交易日计算所有股票当天的因子值对因子值做截面排序Rank对下期收益也做截面排序Rank计算两组 Rank 之间的 Spearman 相关系数即当日 Rank IC对所有交易日取均值得到 IC 均值IC 均值除以标准差得到 IR。下面是 Rank IC / IR 计算的完整代码# 文件路径quant_research/metrics/ic_analysis.py import pandas as pd import numpy as np from scipy.stats import spearmanr def calculate_rank_ic(factor_df: pd.DataFrame, return_df: pd.DataFrame) - pd.Series: 逐日计算因子值与未来收益的 Rank IC。 参数 ---- factor_df : DataFrame index 为 trade_datecolumns 为 stock_code值为因子值。 return_df : DataFrame 与 factor_df 同形状值为未来 N 日收益。 返回 ---- ic_series : Series 每个交易日的 Rank IC。 ic_list [] date_list [] # 两个 DataFrame 的 index 必须严格对齐 common_dates factor_df.index.intersection(return_df.index) for dt in common_dates: factor_row factor_df.loc[dt].dropna() return_row return_df.loc[dt].reindex(factor_row.index).dropna() # 再次取交集保证二者样本一致 valid_idx factor_row.index.intersection(return_row.index) if len(valid_idx) 30: continue f_vals factor_row.loc[valid_idx] r_vals return_row.loc[valid_idx] ic, _ spearmanr(f_vals, r_vals) if np.isnan(ic): continue ic_list.append(ic) date_list.append(dt) return pd.Series(ic_list, indexpd.to_datetime(date_list)) def summarize_ic(ic_series: pd.Series) - dict: 输出 IC 均值、ICIR、IC0 占比、t 统计量等常见指标。 if len(ic_series) 0: return {} ic_mean ic_series.mean() ic_std ic_series.std(ddof1) icir ic_mean / ic_std if ic_std ! 0 else np.nan positive_ratio (ic_series 0).mean() t_stat ic_mean / (ic_std / np.sqrt(len(ic_series))) if ic_std ! 0 else np.nan return { ic_mean: round(ic_mean, 4), ic_std: round(ic_std, 4), icir: round(icir, 4), positive_ratio: round(positive_ratio, 4), t_stat: round(t_stat, 4), sample_days: len(ic_series), }关于 IC 的判断经验可以参考下面这个粗略标准指标弱参考区间说明IC 均值0.02 ~ 0.05常见于日频截面因子绝对数值不大但可能稳定ICIR 0.3说明 IC 序列相对稳定不是少数极端日贡献IC 0 占比 55%说明因子的方向性有持续性t 统计量 2接近统计显著但要注意多重检验问题这只是经验性的参考不能当成数学定理。量化因子的显著性一定要放在具体的股票池和区间里去解释。5.2 分层回测与分组收益单调性IC 描述的是线性相关性但因子与未来收益之间往往不是简单的线性关系。分层回测能弥补这个缺陷。基本做法是每个调仓日按因子值从小到大分为 N 组通常是 5 组或 10 组然后计算每组在下一期的平均收益。如果因子是有效的那么第 1 组到第 N 组的平均收益应当呈现明显的单调性而不是只有中间某一组特别高。下面是 5 分组分层回测的代码# 文件路径quant_research/metrics/group_backtest.py import pandas as pd import numpy as np def group_backtest(factor_df: pd.DataFrame, return_df: pd.DataFrame, groups: int 5): 按因子值分组计算每组的未来收益均值。 参数 ---- factor_df : DataFrame index 为 trade_datecolumns 为 stock_code。 return_df : DataFrame 同一日期和股票的未来收益面板。 groups : int 分组数量默认 5 组。 返回 ---- result : DataFrame index 为 trade_datecolumns 为 group_1 ... group_groups 值为组内股票下一期收益的截面均值。 common_dates factor_df.index.intersection(return_df.index) results {} for dt in common_dates: factor_row factor_df.loc[dt].dropna() return_row return_df.loc[dt].reindex(factor_row.index) valid_idx factor_row.index[return_row.notna()] if len(valid_idx) groups * 10: continue f_vals factor_row.loc[valid_idx] r_vals return_row.loc[valid_idx] # 因子值分位数分组 qcut pd.qcut(f_vals.rank(methodfirst), groups, labelsFalse) 1 grouped_return r_vals.groupby(qcut).mean() # 记录每个分组收益 row {fgroup_{int(g)}: np.nan for g in range(1, groups 1)} for idx, val in grouped_return.items(): row[fgroup_{int(idx)}] val results[dt] row return pd.DataFrame(results).T def monotonicity_score(group_result: pd.DataFrame) - float: 计算分组收益均值与组号之间的 Spearman 相关。 值越接近 1或 -1单调性越强。 group_means group_result.mean() group_nums list(range(1, len(group_means) 1)) corr, _ spearmanr(group_means, group_nums) return corr分层回测中最重要的不是“多头组赚了多少”而是“多空组合第5组 - 第1组的收益曲线是否稳定”。多空收益可以剥离掉大盘涨跌的系统性影响更真实地反映因子本身的选股能力。5.3 单因子方差分析 F 检验从统计显著性上把关IC 和分层回测已经能说明因子排序能力但实践中我还建议加一个统计检验单因子方差分析 F 检验。这个检验的思路是把因子高分组、中分组、低分组的收益分别看成三个样本检验这三组收益的均值是否存在显著差异。如果三组收益均值没有差异说明因子分组后并不能带来收益区分这个因子大概率只是噪声。使用 scipy 的实现非常直接# 文件路径quant_research/metrics/anova_test.py import pandas as pd import numpy as np from scipy import stats def one_way_anova_for_factor(factor_df: pd.DataFrame, return_df: pd.DataFrame, groups: int 5): 对因子分层后的未来收益做单因素方差分析。 返回 F 统计量和 p 值。 p 值越小说明不同因子组的收益均值差异越显著。 common_dates factor_df.index.intersection(return_df.index) all_grouped_returns [] for dt in common_dates: factor_row factor_df.loc[dt].dropna() return_row return_df.loc[dt].reindex(factor_row.index) valid_idx factor_row.index[return_row.notna()] if len(valid_idx) groups * 10: continue f_vals factor_row.loc[valid_idx] r_vals return_row.loc[valid_idx] qcut pd.qcut(f_vals.rank(methodfirst), groups, labelsFalse) all_grouped_returns.append( pd.DataFrame({factor_group: qcut, future_return: r_vals}) ) if not all_grouped_returns: return None samples_df pd.concat(all_grouped_returns, ignore_indexTrue) group_samples [ samples_df.loc[samples_df[factor_group] g, future_return].values for g in range(groups) ] f_stat, p_value stats.f_oneway(*group_samples) return {f_stat: round(f_stat, 4), p_value: round(p_value, 6)}需要注意f_oneway假设各组之间独立且方差齐性对收益率这种尾部较厚的分布只能说近似适用。它不是一个“万能显著性神药”但作为因子初筛的一个依据是足够的。在批量筛选因子时我会把 F 检验的 p 值、Rank IC 的 t 统计量、分层多空收益的单调性三个指标一起看任何一个指标离谱都要警惕。6. 因子库设计把因子变成可检索、可追溯的数据资产研究阶段的因子可以存在内存里、CSV 里、pickle 里但一旦因子要进入策略回测和自动更新就必须进行因子库建设。因子库包含两部分因子数据本身以及因子元数据。数据部分我建议使用数据库表。规模和成本允许时用 PostgreSQL个人研究用 SQLite 也完全可以。关键不是数据库选型而是表结构设计。一个最小可用的因子库至少包含三张表-- 文件路径schema/factor_lib.sql -- 因子定义表记录因子的业务含义和计算公式 CREATE TABLE IF NOT EXISTS factor_definition ( factor_id TEXT PRIMARY KEY, -- 因子唯一标识如 momentum_20d factor_name TEXT NOT NULL, -- 因子展示名 category TEXT, -- 因子大类momentum/volatility/quality/value formula_desc TEXT, -- 因子公式描述 author TEXT, -- 创建人 created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP ); -- 因子值明细表按日期和股票存储因子计算结果 CREATE TABLE IF NOT EXISTS factor_value ( factor_id TEXT NOT NULL, trade_date TEXT NOT NULL, stock_code TEXT NOT NULL, factor_value REAL NOT NULL, data_version TEXT NOT NULL, -- 数据版本号用于追溯重建来源 PRIMARY KEY (factor_id, trade_date, stock_code) ); CREATE INDEX IF NOT EXISTS idx_factor_value_date ON factor_value (trade_date); CREATE INDEX IF NOT EXISTS idx_factor_value_stock ON factor_value (stock_code); -- 因子检验报告表每次批量检验后的总结指标 CREATE TABLE IF NOT EXISTS factor_report ( factor_id TEXT NOT NULL, check_date TEXT NOT NULL, ic_mean REAL, icir REAL, t_stat REAL, anova_f REAL, anova_p REAL, monotonicity REAL, sample_start TEXT, sample_end TEXT, source_version TEXT, PRIMARY KEY (factor_id, check_date) );这个表结构最大的好处是因子值和因子定义分离。因子定义是稳定的元信息因子值是不断更新的计算结果。每次重新计算因子时如果数据源或计算公式变化就更新data_version这样即使同一时间点的因子值在两条记录里不一样你也能说清楚哪一条是哪个版本算出来的。在数据量比较大的时候按(factor_id, trade_date, stock_code)做复合主键会导致索引偏大可以改成自增主键 普通索引但研究阶段这个结构已经足够。如果后续要做更复杂的因子管理可以再加一张“因子面板表”把多个因子按(date, stock)组织成宽表方便回测系统直接读入。7. 一个最小因子流水线完整示例把前文的知识整合起来一个可运行的因子流水线大致可以这样串联读原始数据 → 清洗对齐 → 计算因子 → 对齐未来收益 → 计算检验指标 → 写入因子库。下面代码示例用一个模拟数据来跑通整条链路方便你在本地直接复制运行# 文件路径run_factor_pipeline.py import numpy as np import pandas as pd from quant_common.market_data_utils import load_clean_daily_bars, calc_future_return from quant_research.factors.factor_defs import momentum_20d, volatility_20d from quant_research.metrics.ic_analysis import calculate_rank_ic, summarize_ic from quant_research.metrics.group_backtest import group_backtest from quant_research.metrics.anova_test import one_way_anova_for_factor def generate_mock_data(n_stock60, n_days250, seed42): 生成模拟行情方便本地演示整条流水线。 np.random.seed(seed) dates pd.bdate_range(2023-01-02, periodsn_days) stocks [fSH{i:06d} for i in range(1, n_stock 1)] rows [] for stock in stocks: base_price np.random.uniform(5, 100) for dt in dates: close base_price * (1 np.random.normal(0, 0.02)) volume np.random.uniform(1e6, 5e7) rows.append({ trade_date: dt, stock_code: stock, close: close, volume: volume, amount: close * volume, adjust_factor: 1.0, }) return pd.DataFrame(rows) def main(): raw_df generate_mock_data() # 1. 数据清洗 df load_clean_daily_bars(raw_df) # 2. 构造未来收益标签未来 5 日收益 df calc_future_return(df, forward_days5) # 3. 计算因子值并转成截面面板 df[momentum_20d] momentum_20d(df) df[volatility_20d] volatility_20d(df) factor_list [momentum_20d, volatility_20d] for factor_name in factor_list: pivot_factor df.pivot_table( indextrade_date, columnsstock_code, valuesfactor_name ) pivot_return df.pivot_table( indextrade_date, columnsstock_code, valuestarget_return ) print(f\n {factor_name} ) # 4. IC / IR 分析 ic_series calculate_rank_ic(pivot_factor, pivot_return) print(IC Summary:, summarize_ic(ic_series)) # 5. 分层回测 group_res group_backtest(pivot_factor, pivot_return, groups5) print(Group Mean Return:) print(group_res.mean().round(6)) # 6. 单因子方差分析 anova one_way_anova_for_factor(pivot_factor, pivot_return, groups5) print(ANOVA F-test:, anova) if __name__ __main__: main()运行这个脚本之后你会看到类似这样的输出具体数值与模拟数据随机种子有关 momentum_20d IC Summary: {ic_mean: -0.0335, ic_std: 0.0987, icir: -0.3394, positive_ratio: 0.3355, t_stat: -1.3752, sample_days: 245} Group Mean Return: group_1 0.003246 group_2 0.001247 group_3 0.000513 group_4 -0.000834 group_5 -0.002588 dtype: float64 ANOVA F-test: {f_stat: 4.0251, p_value: 0.003091}在这个模拟结果里momentum_20d的 IC 均值为负说明这是一个反向的动量因子反转效应分组收益大致有单调性F 检验 p 值小于 0.05说明分组收益差异显著。这个因子可以进入候选池但它的“负方向”信息是否可用取决于你如何定义多空组合。实际项目里流水线最后还会有一个“写入因子库”的步骤。我在这里不展开写所有数据库操作代码核心思路是把factor_definition里的元数据插入定义表把pivot_factor重构成长表之后写入factor_value把上面打印的检验结果写入factor_report。注意在写入前先按factor_id data_version删除旧版本数据或者做版本分区避免数据重复。8. 常见问题与排查手法因子研究走到工程化阶段踩过的大部分坑都有共性。下面列几个我经历过的高频问题。问题现象可能原因排查方式解决方案因子回测曲线很好实盘完全失效前视偏差或幸存者偏差检查因子计算时是否用了未来数据确认股票池过滤是否用了未来状态严格区分因子计算时点与未来收益时点同一个因子在 A 股和港股表现完全不同交易制度差异涨跌停、T1、停牌规则不同对照两边的行情数据口径检查是否统一处理了涨跌停按市场单独调整处理规则不要一套代码通吃因子库里出现大量 NaN 因子值数据缺失、停牌、上市时间不足检查样本筛选逻辑分析 NaN 出现在哪些股票和日期明确“不可交易日”的处理策略决定是剔除还是填充两次计算结果不一致代码或数据版本变了检查 data_version、git 提交记录、依赖版本引入版本控制和环境锁定多重检验后显著因子仍然失效参数遍历过多纯数据挖掘检查是否做过样本外检验、滚动窗口检验增加样本外验证、减少参数组合、使用 walk-forward 测试第一条是最常见也是最危险的。很多人只在“因子值计算”里小心未来函数却忽略了股票池过滤本身就是前视偏差的来源。比如用 2024 年底的股票列表去回测 2020 年的历史因子已经退市的股票全部被排除这个样本本身就被污染了。第二条在跨市场研究中尤其明显。A 股有涨跌停限制一字板买不进港股没有涨跌停但可以有瞬间暴跌。如果忽略这些交易机制单纯拿收益率做因子很容易做出在实盘无法复现的结果。第三条容易被忽略。因子库里NaN不是一个坏数据而是一个重要信号它代表这只股票在这个时间点上没有计算因子所需的数据。处理方式会影响因子值分布也会影响后续模型训练。如果一个因子在某只股票上经常无值直接填充 0 往往是错的做法。9. 量化因子库建设最佳实践因子库是整个量化研究体系里面“一次投入长期受益”的基础设施。基于这段时间的实践我把对工程和流程的建议总结成几条。第一因子代码必须有测试。哪怕只是assert系列长度的简单测试都要写。很多因子错误不是公式错误而是数据对齐错误。一个基本测试是对单只股票手动计算一段已知窗口的因子值与代码输出比对。现在不做测试后面整个因子库都会建筑在流沙上。第二因子元数据必须完整。我见过很多因子库里只有因子值和股票代码完全没有公式描述、依赖数据源、创建人、创建日期。一旦原始数据源升级或者行情接口发生变化这些信息缺失会造成巨大损失。元数据表哪怕只有几十个字段也比什么都没有强得多。第三样本外与滚动检验是底线。任何因子在上线前至少应该做一次 1 年样本外验证。因子研究中的多重检验问题非常隐蔽你试了 200 个参数组合总有几个看起来显著这是随机性导致的。处理好这个问题的方法不是把 p 值调严而是做滚动窗口检验观察 IC 的稳定性是否随窗口变化而衰减。第四重视“数据版本管理”。行情数据不是一成不变的财务数据会有修正复权因子会随分红送股调整而更新。如果因子库不记录数据版本数据源升级可能导致全库结果变化而你毫无感知。最直接的做法是给每天生成的数据打一个类似data_20250101_v2.4的版本号并写入因子值表和因子报告表。第五权限与安全边界要提前划定。因子是研究团队的长期资产不建议所有人都能直接修改生产因子库。最小权限原则在这里同样适用研究员可以提交因子定义到草稿区只有负责人评审通过后才能发布到正式因子库。写代码时也要注意任何清空表、批量更新操作之前务必先备份目标表。10. 总结与后续学习方向因子阶段收官收的不是“把因子算完”这个动作而是把散乱的代码和数据沉淀成一套能自我解释、能批量复现、能被策略层直接调用的系统。从数据流到因子库的完整链路表面上是一个工程搭建过程本质上是一个“研究思维工程化”的过程先定义清楚数据口径再用统一的算子实现因子接着用 IC、分层回测和方差分析从不同角度检验最后用元数据把结果固化下来。如果你正在做量化研究下一个阶段无论继续深挖因子还是转向组合优化与回测系统这套链路都不会白搭。你可以从一个小切口开始挑一个你最近研究过的因子给它补上公式定义、数据版本和检验报告并写一个最小脚本让它从原始数据自动重建。完成这一件事你的因子研究就进入了一个新的阶段。之后值得继续深入的方向有三个一是多因子合成与风险中性化把多个低相关因子合成一个复合因子二是因子收益归因看一个策略的收益究竟来自哪些风格暴露三是把因子库接入回测与组合优化系统让研究端和生产端不再手工搬运数据。这些方向的基础都是今天这篇文章里讲到的“数据流 → 因子 → 检验 → 因子库”这套框架。
返回列表