首页 / 资讯中心 / 文章详情

Python多因子选股模型全解析:因子合成、IC加权与组合优化实战

Python多因子选股模型全解析:因子合成、IC加权与组合优化实战 ★ FEATURED ARTICLE
简介Python量化投资基础教程第七章课件聚焦多因子选股模型适合量化交易学习者与金融从业者快速搭建因子研究框架。课件共27页以单个pptx文件提供压缩包大小约1.46MB。内容从细分因子相关性分析入手讲解Pearson与Spearman相关系数、IC值变化方向的意义进而介绍等权法、历史收益率加权、信息系数加权、PCA降维与逐步回归等因子合成方法并给出合成后因子的回归法与IC法检验思路。随后阐释因子权重确定的多种策略如IC均值加权、IC_IR加权及最大化复合IC_IR再结合打分法构建组合并进行回测最后通过二次规划完成组合优化形成完整的多因子选股流程。该教学课件已吸引247人学习适合作为课堂教学补充或自学参考尤其有助于理解因子分析、组合构建与风险优化的Python实现逻辑。1. 多因子选股的逻辑起点因子越多不代表收益越稳多因子选股是Python量化投资建模时绕不开的核心框架很多人一开始就把方向搞反了——拼命往模型里塞因子塞完发现相关性高的因子互相打架合成之后还不如单一因子。那份量化投资教学课件的第七章把问题拆成三个层面大类因子怎么合成、多因子选股模型怎么构造、ATRADTM指标的策略怎么落地。这篇拆解面向正在学Python量化分析、想从课件快速过渡到可运行代码的初学者也面向想搞懂因子加权、IC_IR和二次规划应用边界的老手。课件本身也是金融专业常用的教学模板素材拿来练手和套框架都比较合适。2. 大类因子合成相关性分析、三种加权与PCA降维实操多因子模型的第一步不是选因子而是先检查同一大类下面的细分因子是不是在用同一套信息。课件第一章讲的大类因子合成核心就是相关性分析加信息提取。2.1 Pearson和Spearman相关系数什么时候优先用哪个课件里明确说明因子相关性可以用Pearson和Spearman两种方法计算。Pearson衡量的是两个因子暴露向量的线性相关性公式是协方差除以标准差Spearman则先把因子暴露按截面排序取秩次替代原始值再做相关性计算。区别在实际数据里非常明显如果因子暴露近似正态分布两个系数的差别不大如果因子暴露存在尖峰厚尾换手率、波动率类因子常见少数极端值会明显抬高Pearson系数Spearman因为只关心秩次极端值的影响就小得多。我的操作习惯是把两个值都打出来差值超过0.15就要回头查数据里有没有极端暴露值。import numpy as np from scipy.stats import pearsonr, spearmanr # 模拟两个细分因子的截面暴露人为加一个极端值 np.random.seed(2024) factor_a np.random.normal(0, 1, 500) factor_b factor_a * 0.6 np.random.normal(0, 1, 500) * 0.8 factor_b[0] 12.0 p_r pearsonr(factor_a, factor_b)[0] s_r spearmanr(factor_a, factor_b)[0] print(fPearson{p_r:.4f}, Spearman{s_r:.4f}, 差值{abs(p_r-s_r):.4f})这里的seed是模拟数据的随机种子用于固定复现。factor_b[0] 12.0人为制造极端暴露用来演示两个相关系数的差异。单看Pearson可能得出“中高度相关”的结论Spearman则会把极端值的影响压下来。课件里还补了一个判断维度如果细分因子的IC值整体变化方向一致说明因子之间存在显著相关性。IC方向比单期相关系数更稳健因为它把因子预测能力和相关性挂钩了。IC值整体方向一致和相关系数高这两个信息可以互相印证。2.2 三种加权合成方式等权、历史收益率加权、IC加权确认因子之间高相关之后下一步是把细分因子合成大类因子。课件给了三种固定权重的合成方式等权法所有细分因子权重相同。实现简单不引入主观判断但忽略了因子质量差异。历史收益率加权法按因子各自的历史收益率分配权重。逻辑直接但容易被短期行情主导某因子在极端行情里赚过大钱权重就会异常偏高。信息系数加权法按因子IC均值分配权重。IC衡量的是因子对股票收益的预测能力这比历史收益率本身更贴近“有效性”。import pandas as pd from scipy.stats import spearmanr # 两个细分因子的截面暴露 returns pd.DataFrame({ vol20: factor_vol20, turnover: factor_turnover }) forward_ret pd.Series(future_return) # 计算每个因子的IC ic_vol20 spearmanr(returns[vol20], forward_ret)[0] ic_turnover spearmanr(returns[turnover], forward_ret)[0] weights pd.Series([abs(ic_vol20), abs(ic_turnover)]) weights weights / weights.sum() # 加权合成大类因子 composite weights[0] * returns[vol20] weights[1] * returns[turnover] print(IC加权权重:, weights.round(4).to_dict())IC加权时一定要对IC取绝对值再归一因为负IC的因子同样有很强的预测能力只是方向相反。等权法和IC加权法建议都保留等权作为基准版本加权作为候选版本最后在分层回测里比较差异再做取舍。实际操作中我一般先跑等权版本看因子是否稳定再切到IC加权版本对比。如果两种方式的合成结果在分层回测里差异很小说明这组细分因子内部信息高度重叠用哪套权重差别不大如果差异明显就得仔细查因子的数据质量了。2.3 PCA降维合成情绪因子70%方差解释率怎么落地课件里第二种因子合成方式是主成分分析。把相关性高的细分因子降维用主成分作为大类因子的代理变量方差解释率最低要求70%。课件举的例子是情绪因子用VOL20这一类细分因子跑PCA输出emotion因子。实际跑的时候要先把因子暴露标准化否则方差会被量纲大的因子主导。不同因子量纲不一致时直接用原始值跑PCA第一主成分基本会被某个量纲大的因子控制住。from sklearn.decomposition import PCA from sklearn.preprocessing import StandardScaler # X行是股票列是同一个大类下的细分因子暴露 X pd.DataFrame({ vol20: factor_vol20, vol60: factor_vol60, turnover: factor_turnover }).to_numpy() X_scaled StandardScaler().fit_transform(X) # n_components0.70 表示保留至少70%方差 pca PCA(n_components0.70) pca.fit(X_scaled) print(主成分数量:, pca.n_components_) print(各主成分方差解释比:, pca.explained_variance_ratio_) emotion pca.transform(X_scaled)n_components0.70这个参数是方差解释率的阈值sklearn会自动选能满足阈值的最少主成分数量。如果第一主成分已经解释了65%第二主成分贡献5%就会保留两个主成分而不是强行压缩成一个。输出的emotion就是合成后的大类因子暴露矩阵。PCA合成的因子回测效果通常不错但主成分是原始因子的线性组合经济含义不清晰后面第五章会专门讲这个坑。在验证情绪因子的IC之前先看pca.components_判断哪些原始因子主导了主成分方便后续解释。2.4 合成因子有效性检验回归法和IC分析的边界大类因子合成后课件要求重新检验因子之间的相关性。此时因子之间不再有相似的经济含义如果仍出现明显相关性就要根据因子有效性进行取舍。这保证多因子模型在经济含义和收益效果两方面同时达到最优。检验方法课件给了两个回归法把股票收益率对因子暴露做回归因子收益率代表这个因子能贡献多少收益。课件特意提醒因子收益率本身不包含对可能性的判断。IC法IC值本质上是因子暴露与股票收益率的线性相关系数。IC绝对值越接近1说明因子预测收益越稳定波动越小。# 合成后大类因子的IC ic_value, p_value spearmanr(emotion[:, 0], stock_returns) print(f合成因子IC: {ic_value:.4f}, p值: {p_value:.4f})IC的p值在金融截面数据里通常很小因为样本量大但p值小不代表有效性高。真正更有用的是IC的稳定性建议连续跑12期滚动IC看IC均值和IC_IR。如果IC只在牛市月份为正值熊市月份翻负这个因子在收益增强上没什么意义。做分层回测时看分层收益的单调性比单看IC绝对值更靠谱。合成后的因子最终要经过第三章的权重确定所以检验环节要保留每个候选因子的IC_IR值到时候权重计算直接复用。3. 多因子模型构造权重确定、打分选股与回测分析课件第二章“多因子选股模型构造”开始进入策略层面。核心是三个环节确定因子权重、构建组合、回测分析。3.1 因子权重确定的四种方法从等权到最大化复合IC_IR大类因子的权重不是随便定的。课件给了四种方法从简单到复杂。第一种是等权处理。课件特别指出以往研究表明估值因子和规模因子表现相对显著杠杆因子和运营因子相对较弱等权处理会忽略不同因子的解释力度。第二种是IC均值加权按因子有效性分配权重表现显著的因子分到更大权重。第三种是IC_IR加权。IC_IR定义为IC均值除以IC标准差综合考虑了因子有效性和稳定性在保证收益的同时也考虑了波动。第四种是最大化复合因子IC_IR。以历史一段时间的复合因子平均IC值作为下一期IC值的估计以历史IC的协方差矩阵作为波动率估计解出最优权重。从工程落地角度IC_IR加权是性价比最高的选项不需要求解最优化问题但已经比单纯用IC均值更抗噪声。权重方法核心思想参数依赖等权法所有因子权重相同无IC均值加权按预测能力IC均值分配权重IC序列IC_IR加权按有效性和稳定性IC均值/IC标准差分配权重IC序列最大化复合IC_IR最优求解复合因子的IC_IR历史IC均值、IC协方差矩阵表格里的参数依赖决定了方法的复杂度。实盘里很多团队只用IC_IR加权把收益率目标交给组合优化那一步去处理不把因子权重和组合权重两套逻辑纠缠在一起。# 两个大类因子的历史IC滚动序列 ic_1 pd.Series([0.03, 0.02, 0.05, -0.01, 0.04]) ic_2 pd.Series([0.01, 0.04, -0.02, 0.03, 0.06]) ic_ir_1 ic_1.mean() / ic_1.std() ic_ir_2 ic_2.mean() / ic_2.std() weights pd.Series([ic_ir_1, ic_ir_2]).abs() weights weights / weights.sum() print(IC_IR权重:, weights.round(4).to_dict())这里除以标准差是关键它把IC的波动惩罚进去了。如果你看到某个因子的IC_IR不到0.3说明这个因子的超额收益基本被波动淹没给它分配权重只会增加组合噪声。3.2 因子数据预处理四步缺失值、离群值、中性化、标准化模型构造的第一步其实是处理原始因子暴露。课件给出的流程是缺失值填充、离群值处理、中性化、标准化。这个顺序不能乱。缺失值一般用行业均值或中位数填充。离群值处理用MAD缩尾比3sigma更抗干扰。中性化是对行业和市值做回归取残差这一步最容易跳过但影响很大。import numpy as np import pandas as pd import statsmodels.api as sm def winsorize(s, n3): MAD缩尾用绝对中位差识别离群值 median s.median() mad (s - median).abs().median() lower median - n * 1.4826 * mad upper median n * 1.4826 * mad return s.clip(lower, upper) def neutralize(factor, industry_dummies, log_market_cap): 行业市值中性化取残差 X pd.concat([industry_dummies, log_market_cap], axis1) X sm.add_constant(X) model sm.OLS(factor, X).fit() return model.resid # 使用示例 factor_clean winsorize(raw_factor, n3) factor_neutral neutralize(factor_clean, industry_dummies, log_mktcap) factor_standard (factor_neutral - factor_neutral.mean()) / factor_neutral.std()MAD缩尾参数n3的意思是偏离中位数3倍MAD外的值被裁剪到边界1.4826这个常数把MAD换算成标准差。中性化完成后注意检查残差的行业均值是否接近零如果某个行业仍然显著偏高说明行业哑变量的构造有问题。中性化不做的后果是选出来的股票集中在某个行业或某个市值区间表面是多因子选股实际是一个行业基金。这种问题不是看IC能发现的要实盘检查持仓风格暴露的时候才能看出来。3.3 打分法与30组分组选股前20%股票池怎么划课件里说策略只有两个大类因子为了更好地区分股票将沪深300成分股按综合分值分成30组选取综合分值排名前20%的股票买入。这里有两个关键设计。一是用分位数排名替代原始分值消除量纲影响。二是分组数量决定区分粒度30组比10组更细但要求股票池足够大不然每组统计量太少。沪深300有300只成分股30组大概每组10只前20%对应60只左右。# 加权合成两个大类因子分数 score_1 factor_neutral_1.rank(pctTrue) score_2 factor_neutral_2.rank(pctTrue) total_score w1 * score_1 w2 * score_2 # qcut分30组取最后6组作为前20% groups pd.qcut(total_score, 30, labelsFalse) selected total_score[groups 24].indexgroups 24表示第25组到第30组正好是30组中的最后20%。这个阈值写死前要确认分组数量一旦换成10组阈值就要变成groups 8。我在自己项目里会把分组数和阈值做成参数方便复现和调整。3.4 回测结果和止损失效分析年化28.17%为什么不理想课件的回测设置是日线行情区间2019年全年基准为沪深300指数。最终策略年化收益28.17%基准年化收益31.33%胜率31.67%。单看年化28%好像尚可但跑不赢基准胜率不到三分之一意味着绝大多数时间里策略都在追赶基准。课件对净值曲线的分析很关键策略下跌时跌幅很大起初怀疑止损无效检查后发现止损是生效的但止损的同时又买入了新的股票。市场整体行情向下时个股选择对收益的贡献很小新买入的股票同样在跌止损效果被变相抵消。这个问题本质是资金管理逻辑的问题和止损参数的关系不大。我的改进做法是止损触发后设置冷却期冷却期内不开新仓。市场单边下行趋势没有结束前仓位应主动降到防守线而不是按打分结果继续买入。4. 组合优化与二次规划约束下的权重求解实例打分法选出前20%的股票池后课件引出了组合优化在满足风险和其他约束的前提下最大化收益或在满足收益约束的前提下最小化风险。这里用的是二次规划。4.1 二次规划的标准形式P、q、G、h、A、b矩阵怎么构建课件给出的二次规划方程中w是待求解的股票权重列向量R是股票预期收益列向量T代表向量转置e是目标收益值XF是股票关于因子F的因子暴露列向量XF,L和XF,u是因子暴露上下限约束。标准二次规划在数学上由P、q、G、h、A、b六组矩阵描述但Python里用scipy.optimize的minimize更直接把目标函数和约束用lambda表达出来就行。import numpy as np from scipy.optimize import minimize # 三只股票的预期收益与协方差 r np.array([0.179, 0.131, 0.283]) cov np.diag([0.048, 0.189, 0.139]) def portfolio_risk(w): return w cov w def portfolio_return(w): return r w cons [ {type: eq, fun: lambda w: w.sum() - 1}, {type: ineq, fun: lambda w: portfolio_return(w) - 0.15} ] bounds [(0.1, 0.5)] * 3 x0 np.array([1/3, 1/3, 1/3]) opt minimize(portfolio_risk, x0, boundsbounds, constraintscons) print(最优权重:, np.round(opt.x, 4))portfolio_risk是目标函数求最小化组合方差约束1要求权重之和等于1约束2要求预期收益率不低于15%bounds限制每只股票权重在0.1到0.5之间x0是优化起始点给均值权重即可。scipy的minimize把二次规划的标准矩阵内部消化了代码更易读。但理解标准形式仍然必要排查问题时要能把约束条件映射回原方程。4.2 三只股票的权重求解最优权重为什么落在边界课件案例中三只股票的预期收益分别为0.179、0.131和0.283方差分别为0.048、0.189和0.139三只股票互不相关、协方差为0。约束是预期收益率不小于15%个股权重位于[0.1, 0.5]权重之和为1。运行上面的代码得到的最优权重约为[0.5, 0.212, 0.288]。第一只股票的权重正好压在0.5的上界上。原因不难解释这只股票的方差最低、风险最小在最小化风险的目标下优化器会尽量把权重分给低波动资产最终被上界约束卡住。边界解在组合优化里很常见。它提醒我们约束太紧时最优解会贴在边界上组合集中度可能超出实际预期。所以在约束设计时除了给个股上下限通常还要加行业权重约束和因子暴露约束。4.3 常见约束条件行业权重、因子暴露和个股上下限课件列出了三类常见约束行业权重约束、因子暴露约束、个股上下限约束。风险和收益的条件约束可以是等权时的风险和收益也可以指定某一特定水平。行业权重约束解决行业过度集中的问题。因子暴露约束限制组合在市值、β等风格因子上的暴露避免无意中变成风格基金。个股上下限约束防止权重集中到单只股票上。# 假设industry_vec是行业归属向量0/1编码 cons_industry {type: ineq, fun: lambda w: 0.2 - abs(w industry_vec)} cons_factor {type: ineq, fun: lambda w: 0.3 - abs(w factor_expo_vec)}行业约束和因子约束都用绝对值形式允许正向偏离也允许负向偏离但偏离幅度限制在一定范围内。这些约束可以加到上一节的constraints列表里优化器会一起求解。约束加的多了初始值x0对结果的影响变大建议用多个随机初始值跑几遍确认最优解稳定。5. 避坑记录量化因子模型里最容易翻车的五个场景这部分是课件之外我用血泪换来的踩坑经验每一条都曾让回测结果和实盘表现差距拉大。5.1 因子共线性相关系数低不代表不共线现象两个因子的Pearson和Spearman都在0.3以下回归模型里VIF方差膨胀因子却超过10因子系数符号随样本变化剧烈。原因相关系数只捕捉两两之间的关系。多个因子经过线性组合后可能产生多重共线性此时相关系数矩阵看起来正常回归模型里的VIF却很快爆表。解决定期对候选因子计算VIFVIF大于5的因子优先剔除。这也是课件强调高相关因子要合成的深层原因——消除共线性比降低两两相关性更重要。5.2 止损失效卖出和买入同时发生回撤反而变大现象止损单触发后净值继续快速下行回撤幅度甚至超过不设止损的情况。原因回测逻辑里止损卖出旧股票的同时策略按新打分结果买入了新股票。市场整体下行时新买入的股票继续下跌止损效果被买入动作对冲掉了。解决止损触发后设置冷却期冷却期内不做买入操作。市场趋势向下时优先降低总仓位而不是换一批股票继续满仓运行。5.3 IC方向不稳定均值是正数逐期却忽正忽负现象因子IC均值为正值滚动IC序列却是正负交替累计IC曲线在0附近反复穿越。原因因子在特定市场风格下有效在反转行情下失效。单纯看IC均值掩盖了时间维度上的波动。解决统计IC为正的期数占比低于60%的因子不要直接进模型再按市场风格分开统计IC确认真实有效环境。这种因子说得好听一点叫周期性失效说得直白一点就是阶段性玄学信号别让它在模型里占有权重。5.4 PCA主成分的语义丢失降维后解释不了现象PCA合成的大类因子回测效果不错但写策略报告时说不清楚因子的经济含义。原因主成分是原始因子的线性组合第一主成分可能混合了换手率、波动率、流动性等多类信息语义被分散了。解决研究阶段优先用等权或IC加权法合成保留可解释性。PCA适合放在风控模块做风险压缩不适合直接参与投资决策。如果必须用PCA至少打印pca.components_看哪些原始因子主导了主成分。5.5 数据泄露未来函数才是回测曲线里看不见的“主力”现象回测收益高得离谱曲线平滑像教科书案例模拟盘和实盘却对不上。原因因子计算和交易信号之间存在时间错位。比如用当日收盘价计算因子再按当日开盘价买入相当于用了未来数据。股票池没有剔除退市股票也会造成幸存者偏差。解决T日因子必须基于T-1日及以前的数据计算T日日初执行交易。回测结束后做一次置换检验把收益序列打乱重新跑如果策略仍然保持高收益说明回测逻辑里很可能有数据泄漏。6. 实战延伸ATRADTM组合指标的构建与验证要点课件最后一部分落到ATRADTM策略上把波动率类和情绪类指标结合进选股。ATR衡量真实波幅ADTM是动态买卖气指标二者本质上对应波动率因子和情绪因子的实战版本。ATR要先算真实波幅TR当天最高价减最低价、最高价减前收盘价、最低价减前收盘价的绝对值三者取最大再做N日均值。ADTM则根据开盘价、最高价、最低价和收盘价的关系计算累计买卖气。两个指标在量化框架里的落地方式仍然是先算因子暴露、再做四步数据处理、最后跑IC验证和分层回测。参数方面ATR窗口常见14天ADTM窗口常见7天。不要只看默认参数我通常把ATR窗口按10、14、20三档跑一遍ADTM按5、7、10三档跑一遍对比IC和分层回测结果再做选择。窗口太大指标太钝窗口太小噪声太多最优窗口在不同市场环境里是变化的。def calc_atr(high, low, close, n14): tr1 high - low tr2 (high - close.shift()).abs() tr3 (low - close.shift()).abs() tr pd.concat([tr1, tr2, tr3], axis1).max(axis1) return tr.rolling(n).mean() def calc_admt(high, low, open_price, close, n7): cond open_price close d_m np.where(cond, high - open_price, high - close) d_s np.where(cond, low - open_price, low - close) dm_sum pd.Series(d_m).rolling(n).sum() ds_sum pd.Series(d_s).rolling(n).sum() adtm (dm_sum - ds_sum) / (dm_sum ds_sum) return adtm代码里rolling(n).mean()的n是ATR的窗口长度rolling(n).sum()的n是ADTM的累计窗口dm_sum减ds_sum反映买卖气的净力度除以总和归一化。注意两个指标都要在截面层面做行业中性化和市值中性化否则选出来的股票会被波动率风格带偏。这类指标的IC方向在趋势市和震荡市里经常相反验证时不能只看全样本IC要拆成两段时间分别看。量化策略没有一劳永逸的模型。从那以后我每次拿到一套课件或者现成的因子模型都会强制走一遍完整验证流程先把数据时间戳对齐再跑IC稳定性最后做样本外验证。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站