
1. 为什么我推荐你用Python做多因子选股过去几年越来越多朋友开始在后台问我同一个问题想学量化交易方向太多、资料太杂到底从哪里入手我的答案一直没有变过——先做多因子选股模型。原因不复杂股票量化的核心无非两件事一是预测股票未来的相对强弱二是用组合的方式把预测转化成收益。多因子选股恰好把这两件事都覆盖了而且所有环节你都能用Python独立完成不需要券商柜台系统也不需要机构级的行情接口普通电脑完全带得动。先给零基础的朋友定位一下这篇文章的目标不是让你实盘躺赚而是帮你完整走通“数据获取→因子构造→打分选股→回测验证”这条链路手里掌握一套能自己持续迭代的研究框架。我在线下带过不少学员有些人一开始就追着机器学习、深度学习跑结果连因子是什么都没搞清楚最后全卡在数据预处理上。所以我特别建议你先用多因子模型把基本功打扎实它简单、透明、可解释性强比直接上神经网络靠谱得多。那为什么偏偏是Python因为这门语言在量化领域已经形成了事实标准。pandas处理表格数据、numpy做数值计算、matplotlib画图、tushare和akshare拉数据生态链非常完整。你只需具备最基本的Python语法知识能看懂循环、函数、DataFrame操作就能跟着下面的步骤跑通整个模型。如果你连Python环境还没装好先去官网下载最新稳定版然后通过pip install安装依赖库整个过程配一杯咖啡的时间就够。这个模型能做什么它的本质是帮你回答一个问题在每个月或每周的调仓时点全市场几千只股票里哪一批最值得买。它把估值、动量、质量、流动性等不同维度的信息转化为一个个“因子”再按一定的权重把因子汇总成一个总分通过总分排序选出排名靠前的股票构建组合。整个逻辑很像相亲时综合身高、收入、性格去打分——单一标准容易看走眼多个维度一起看胜算高得多。文章后面给出的代码我会尽量保证它不是“教学玩具”而是贴近真实研究场景的版本包含数据缓存、因子去极值、市值中性化、分层回测等环节。你完全可以把代码另存为一个.py文件按顺序跑起来然后再逐步改成你自己的策略。接下来我先讲清楚动手之前必须想明白的几件事再进入实操环节这样你后面写代码时会非常顺畅。2. 动手前必懂的知识储备避免从入门到放弃2.1 先搞清楚“因子”到底是什么很多人一听到“多因子模型”就以为是什么高深算法其实一句话就能解释因子就是一条能够区分股票好坏的特征或指标。比如市盈率它就是一个价值因子。PE低通常意味着股票相对便宜便宜不一定马上涨但长期来看估值回归的概率较高。再比如过去一个月的涨跌幅这就是动量因子反应的是市场趋势对股价的延续效应。单因子的毛病在于不稳定。某一段时间低估值股票表现极好另一段时间却完全不涨动量也是这样震荡市里面它经常变成反向指标。所以我们要把多个逻辑上互补的因子组合起来让它们互相牵制、互相补充。常用的横截面因子大致有这么几类估值类因子市盈率、市净率、市现率、股息率等捕捉“便宜”的机会。质量类因子ROE、毛利率、资产负债率、经营现金流等关注公司赚钱能力和财务健康程度。动量类因子过去20日、60日收益率、卡玛比率等刻画趋势和惯性。规模类因子总市值、流通市值代表大小盘风格。流动性因子换手率、成交额、Amihud非流动性指标等防止买入那些根本卖不出去的股票。这五类因子各有各的经济学逻辑没有哪个永远跑赢这也是多因子模型长期有效的根本原因——市场风格总在轮动多维度打分能让你在不同市场阶段都有所覆盖。2.2 横截面思维同一时刻比较全体股票单因子分析看的是“这只股票的这个指标高不高”多因子模型看的是“在所有股票里谁的这个指标更突出”这就是横截面思维。你可以想象给学生排名单科成绩不说明问题按综合分排序才有意义。量化里有一个专门的工具叫截面排序我们每个月末把所有股票的因子值拿出来按从小到大的顺序排列然后转成百分位排名。为什么要用排名而不是原始数值因为原始数值量纲不同。PE的数值是几十倍ROE的数值是百分之十几换手率又完全不一样直接相加等于把大数量纲的因子绝对主导了这肯定不对。排名转换之后每个因子的取值都统一到0到1之间含义变成“这只股票在所有股票中处于什么位置”这样才有可比性。2.3 收益预测只是第一步组合构建才是关键很多新手以为选出一篮子股票就完事了其实收益预测只是给股票打了分真正决定你最终收益的是打分之后的组合构建和权重分配。这里面有几个绕不开的问题第一选多少只股票。选得太多组合贴近指数超额收益被稀释选得太少波动巨大单只股票的负面消息就能毁了整个组合。对于普通资金量来说10到30只比较合适。第二权重怎么分配。等权最简单市值加权贴近指数资金加权体现“越看好越买得多”的认识。新手我建议用等权起步减少主观判断。第三调仓频率。每20个交易日调一次仓比较主流既能捕捉月度级别的变化又不至于被交易成本吞噬。调仓太频繁交易成本是最大的敌人。2.4 数据源选型这件事比你想象的更重要量化研究有个铁律模型好不好先放一边数据不行一切白搭。就个人开发者而言我主力推荐两个数据源。第一个是Tushare Pro专业度最高财务数据非常全但积分门槛需要你花一些时间积累不少关键接口需要2000积分以上。第二个是Akshare完全免费无需积分数据来源是公开网页胜在便捷、接口数量极多缺点是偶尔因为网页改版导致接口失效需要等更新。我的建议是你把两个都用上akshare作为日常研究的主力tushare作为财务数据不可用时的备选。代码里我用的是akshare的接口原因就是你复制代码之后不需要注册token、不需要等待积分审批当天就能把数据跑起来。跑通了整套流程之后你再去换tushare或其他商业数据源打磨细节。3. 环境准备与数据基建把地基打牢3.1 虚拟环境与依赖安装我不太建议你把所有项目的东西一股脑装到全局Python环境里量化项目的依赖版本非常容易打架。用虚拟环境隔离避免“跑你这套代码把另一个项目的库搞挂了”这类惨剧发生。命令行里执行mkdir multi_factor_quant cd multi_factor_quant python -m venv venv source venv/bin/activate # Windows系统执行 venv\Scripts\activate激活环境之后安装依赖pip install pandas numpy matplotlib akshare scipy statsmodels tqdm这几个库的用途分别说一下pandas和numpy是做表格处理与数值计算的主力matplotlib负责画净值曲线和因子分析图akshare负责拉行情和财务数据scipy和statsmodels在后面的因子标准化、回归中性化等环节会用到tqdm用来显示循环进度避免你盯着黑屏干等。3.2 定义股票池与行情获取函数多数A股量化策略的第一步是先圈定一个基础股票池。全市场5000多只股票里很多刚上市、长期停牌或者已经ST的股票并不适合直接进策略池。我们先把沪深两市的全部A股代码拉出来然后做一层粗过滤剔除ST、剔除上市不满60个交易日的次新股、剔除停牌或者长期无成交的股票。这里给出获取代码的函数返回的stock_list是干净的待研究股票池import akshare as ak def get_stock_list(): # 获取A股实时行情数据里面包含了全部股票的代码、名称、最新价、市值等信息 df ak.stock_zh_a_spot_em() # 代码列过滤剔除北交所股票8开头和4开头的都属于北交所 df df[~df[代码].astype(str).str.startswith((8, 4))] # 名称过滤剔除ST、退市整理期股票 df df[~df[名称].str.contains(ST|退)] # 剔除上市时间不足标的——通过“上市时间”字段筛选这里先保留全部剩余股票 return df[代码].tolist()这里有一个坑提醒大家akshare返回的股票代码是字符串需要在前面统一补零到6位。虽然当前接口一般已经处理好但保不齐版本变化稳妥起见你可以加一行代码做格式化这在数据清洗里属于“宁可多此一举不可事后抓瞎”。行情数据我们按日线拉取为了减少重复请求尽量把全市场的日线数据保存到本地。数据量不大时可以直接放在csv文件里数据量大了建议换用hdf5或parquet格式。下面这个函数负责拉取单只股票的日线数据并对列名做了统一格式化def fetch_daily_price(stock_code, start_date20200101, end_date20240101): # 使用akshare的前复权行情接口前复权保证价格连续避免分红除权造成的价格跳空 df ak.stock_zh_a_hist( symbolstock_code, perioddaily, start_datestart_date, end_dateend_date, adjustqfq ) df.rename(columns{日期: date, 开盘: open, 收盘: close, 最高: high, 最低: low, 成交量: volume, 成交额: amount}, inplaceTrue) df[date] pd.to_datetime(df[date]) df[stock_code] stock_code return df[[date, stock_code, open, close, high, low, volume, amount]]3.3 数据缓存与进度管理行情接口请求频率太高容易被封所以我强烈建议你加一层本地缓存。代码逻辑很简单本地已经有今天的数据就直接读缓存没有才去请求接口。配合tqdm做进度显示一万次请求需要的时间可以接受反正是一次性的def load_all_prices(stock_list, cache_pathdaily_prices.csv): import os if os.path.exists(cache_path): return pd.read_csv(cache_path, parse_dates[date]) all_dfs [] for code in tqdm(stock_list, desc下载日线数据): try: df fetch_daily_price(code) all_dfs.append(df) time.sleep(0.3) # 300ms间隔降低被限制的风险 except Exception as e: print(f{code} 下载失败: {e}) continue result pd.concat(all_dfs, ignore_indexTrue) result.to_csv(cache_path, indexFalse) return result4. 核心因子计算与预处理决定模型成色的关键环节4.1 如何构建三大风格因子数据准备就绪后就可以开始构造因子了。先选取三类有代表意义的因子估值因子EP、动量因子Momentum_20、质量因子ROE它们分别从便宜程度、趋势延续、盈利能力三个维度打分。额外加一个流动性因子用换手率来衡量防止选到那些交易极不活跃的“僵尸股”。专业细节上估值因子我不用市盈率PE直接用它的倒数EPEarnings-Price Ratio原因是PE可能出现负数亏损公司排序时负数公司的处理非常麻烦而EP让数据分布更平滑。动量因子取过去20个交易日的累计收益注意要用前复权价格否则分红除权会让动量瞬间变成负值。ROE数据来自财务报表一般季度更新需要做一次数据对齐。代码部分我们先整理出“价格面板”和“财务面板”。价格面板从3.3节的daily_prices里透视出来每天一行、每只股票一列标准的横截面数据格式财务面板从akshare拉取ROE数据按股票、报告期排列。构造因子前先把停牌日期的价格填充为前值保证连续def prepare_price_panel(price_df): pivot price_df.pivot_table(indexdate, columnsstock_code, valuesclose) # 前向填充处理停牌导致的NaN pivot pivot.fillna(methodffill) # 剔除上市首日等极端前向填充仍为NaN的列 pivot pivot.dropna(axis1, howall) return pivot4.2 因子的去极值与标准化处理原始因子值直接用来打分是有问题的。举个例子某个小市值公司的EP高达200倍而正常公司的EP分布在5到20倍之间这一只股票会把整个排序的分布拉偏。所以在合成因子之前必须做去极值和标准化两步操作。去极值的方法有很多我习惯用MADMedian Absolute Deviation方法。它的原理是计算中位数以及每个值与中位数的绝对偏差再设定一个倍数阈值比如3倍超出阈值就截断成阈值边界值。相比传统3倍标准差MAD对异常值更稳健因为它本身用的是中位数而不是均值不会被极值拉走。scipy里直接有计算MAD的函数完整实现了这个过程from scipy.stats import median_abs_deviation def winsorize_series(s, n5): med s.median() mad median_abs_deviation(s.dropna()) if mad 0: return s lower med - n * 1.4826 * mad upper med n * 1.4826 * mad return s.clip(lower, upper)标准化方式选择横截面标准化对每天的全体股票减去当期均值后再除以当期标准差。这样处理之后每天的因子值都符合均值0、标准差1的分布真正实现了跨时间可比。这里必须强调顺序先去极值再标准化。如果先标准化异常值会把均值和标准差整体拉偏后面的去极值等于白做了。4.3 市值中性化避免你无意中做了一个小市值策略不少新手跑完模型发现收益奇高沾沾自喜地以为发现了圣杯其实仔细一看选出来的全是小市值股票。过去很长一段时间里A股的小市值效应非常明显你赚的可能根本不是因子选股的钱而是小市值的风格收益。一旦市场风格切换这种收益会迅速蒸发。所以要做市值中性化。思路是既然不想要市值因素干扰那就把因子值中对市值回归的残差部分当作真正的因子信号。具体做法就是线性回归拟合因变量为“因子值”、自变量为“市值对数”取残差作为新的因子值。这个残差就是去除了市值影响之后因子中“纯”的那部分信号import statsmodels.api as sm def neutralize(factor_series, market_cap_series): df pd.DataFrame({factor: factor_series, log_mv: np.log(market_cap_series)}).dropna() if len(df) 10: return factor_series X sm.add_constant(df[log_mv]) model sm.OLS(df[factor], X).fit() df[neutralized] df[factor] - model.predict(X) return df[neutralized].reindex(factor_series.index)这套处理流程下来因子的“纯度”就大幅提升了。做完之后可以用IC值做一次体检——IC是因子值与未来收益的截面相关系数绝对值大于0.03就算不错的因子大于0.05属于优秀区间。这部分我放在后面回测里面详细展开。5. 完整实现多因子打分选股与回测框架5.1 月度调仓的整体框架我们的策略框架按月度运行think of it like这个流程每个调仓日计算全体股票的最新因子值做去极值、标准化、市值中性化三步处理然后按权重合成综合因子分最后排序选股。具体到代码就是下面这样一个主循环结构这是整个模型的“发动机”def monthly_rebalance(all_price, stock_pool, factor_df, trade_dates): results [] for date in trade_dates: # 取调仓日最新因子数据 factors factor_df.loc[date].dropna() # 过滤股票池 factors factors[factors.index.isin(stock_pool)] # 合成综合得分 score ( 0.3 * zscore(factors[EP]) 0.3 * zscore(factors[MOM20]) 0.2 * zscore(factors[ROE]) 0.2 * zscore(factors[TURNOVER]) ) # 选取得分最高的前20只 selected score.nlargest(20).index.tolist() results.append({date: date, selected: selected}) return pd.DataFrame(results)这里的权重0.3、0.3、0.2、0.2是我一开始拍脑袋设定的实际工作中需要通过IC加权、滚动优化等方法确定后面回测部分会讲到如何根据结果迭代。5.2 收益计算与净值曲线回测选定股票之后我们需要模拟按月调仓等权持有这些股票到下一次调仓计算这段时间的收益。回测时要特别小心未来函数——调仓日当天收盘后才知道买入名单那就应该以次日开盘价成交而不是当月收盘价。很多业余回测就是死在这里回测结果虚高。我这里采用一个简化处理调仓日收盘时按收盘价成交但要知道这属于“偷看未来”实盘时会略微高估收益。等你后续精细化可以改成次日开盘成交def backtest(returns_panel, rebalance_df): nav 1.0 nav_curve [] hold_stocks [] for i in range(len(rebalance_df) - 1): start rebalance_df.iloc[i][date] end rebalance_df.iloc[i 1][date] select_stocks rebalance_df.iloc[i][selected] period_return returns_panel.loc[start:end, select_stocks].mean(axis1).sum() nav * (1 period_return) nav_curve.append({date: end, nav: nav}) return pd.DataFrame(nav_curve)综合因子分选股的完整框架胜在逻辑清晰、可解释性强。后续你可以从这个基础版逐步升级加入行业中性化、换手率惩罚、风险约束、事件驱动因子等。下面的常见问题部分我会把实际运行中大家问我最多的坑集中讲一遍这几条每一条背后都有真金白银的教训。6. 高频踩坑记录数据、代码与回测中的真实坑6.1 未来函数是回测最大的隐形杀手前面提到一次这里必须再强调一次因为它太重要了。我见过不止一个朋友拿着年化80%的回测曲线兴奋地来找我结果我随口问了一句“你调仓日什么时候买入”对方支支吾吾答不上来。顺着代码查下去十有八九是当天收盘价买入但调仓信号也是当天收盘算出来的。你根本没有可能在收盘那一刻同时完成计算和交易这多出来的收益就是凭空创造出来的。解决思路就是调仓日T发出信号T1开盘或收盘成交整个回测框架里这个逻辑要统一。回测结果宁可保守一点也不要自欺欺人。6.2 财务数据对齐的坑很多人不知道ROE是季度数据股价是日频数据两者直接merge会引入严重的前瞻偏差。比如三季报披露日期通常在10月底但报告期是9月30日。如果直接把“报告期2023-09-30”的ROE匹配到“2023-10-01”之后的股价上那其实已经很接近了但如果匹配到10月初的股价上就会用到当时尚未公开的数据属于典型的前视偏差。正确的做法是用实际披露日期对齐而不是用报告期对齐。akshare的财务数据接口通常包含“公告日期”字段我们应该用公告日期来判断哪一天开始这份财务数据才对市场可见。如果没有披露日期保守处理办法是把数据滞后一个季度再使用例如三季度报的ROE放到次年1月1日才开始生效。损失一点时效性换来的是回测可信度。6.3 停牌、涨跌停与极端值的联合处理回测中最容易被忽视的还有交易限制。你选出20只股票但其中一只连续停牌你想买买不进另一只开盘一字涨停你根本排不上队。这两种情况下理论收益和真实收益能差出好几个点。处理停牌相对简单调仓时过滤掉停牌股票换成备选池里的下一只。涨跌停更麻烦一点需要判断当日是否涨停——对于涨停的股票如果策略要买入默认买不进遇到跌停默认卖不出。基础版本里我建议你至少要过滤掉停牌涨跌停的问题单独写一节来完善。数据层面还有一个很隐蔽的问题前复权价格在除权日那天可能为负值。这是极少数情况下、长期大幅分红的股票才会出现但如果遇到收益率计算会直接报错。稳妥方案是计算收益时统一用pct_change()而不是自己算差分比率让pandas来处理边界情况。6.4 因子有效性检验别等回测完才后悔很多新手把因子构造完就急着合成、回测结果发现组合跑不赢指数回头又不知道该调哪里。正确的做法是先做单因子有效性检验再决定是否把因子加入模型。单因子检验的核心指标是ICInformation Coefficient信息系数就是当前因子值排序与下一期收益排序的Spearman相关系数。IC的绝对值平均在0.03以上就算可以通过初步筛选。除了看IC均值还要看IC的稳定性用IRIC均值 / IC标准差来度量IR大于0.3才算比较稳定。我平时还会画一张IC的月度柱状图如果某个因子的IC在少数几个月特别高、其他月份都是负值说明它的收益来源是偶然事件驱动应该谨慎使用。为了快速看到因子效果你可以把全体股票按因子值分成5组Q1到Q5每组等权持有看Q1组和Q5组的累计收益之差。单调性越好因子越有效如果Q1和Q5经常来回穿插说明因子的区分度不足不值得放在模型里。7. 代码复盘与模型优化方向回测结果出来之后怎么判断模型有没有实际价值先看几个核心指标累计收益率、年化收益率、最大回撤、夏普比率、胜率和换手率。年化收益高但回撤超过30%那它的持有体验极差实盘基本拿不住。夏普比率至少要做到1以上超过1.5就比较理想了。胜率不是最重要的量化交易看的是盈亏比亏的时候少亏、赚的时候多赚比单纯的高胜率更有意义。多因子模型的优化通常有四个方向第一因子库扩充。基础模型里的四个因子只是“开胃菜”真实研究里的因子库往往是几百上千个因子的体量。可以加入基本面因子毛利率变化率、营收增速、经营现金流占总资产比例、技术因子MACD背离程度、RSI位置、布林带宽度、分析师预期因子评级调整幅度、目标价空间等。第二因子加权方式优化。固定权重简单但市场风格切换时表现会比较僵化。可以尝试最近12个月ICIR加权也就是历史表现好的因子给更高权重或者用滚动回归确定最优权重让模型自动适应市场环境。这个问题没有唯一正确答案本质是你在“稳定”和“灵活”之间做取舍。第三组合优化。从打分选前N只的排序法升级为带约束的最优化问题——在给定预期收益的基础上最小化组合波动率或者最大化组合夏普比率同时约束行业暴露、风格暴露和单只股票权重上限。这时候需要引入风险模型通过因子暴露矩阵和协方差矩阵计算组合风险。第四交易成本模型。回测框架里目前还未包含手续费、印花税和滑点实盘里这些成本会严重侵蚀高频调仓策略的收益。建议在回测中按成交金额的千分之一到千分之二预估成本观察策略在扣费后的表现。如果一个策略扣除成本后就没有优势了那调整调仓频率和降低换手率是最直接的改进方向。写到这里整理代码的经历正好让我想起一件事我第一版多因子策略回测年化跑出58%当时还很兴奋后来加上交易成本、滑点和涨跌停限制之后年化直接掉到17%。但这反而让我更信任这个模型——它诚实地展示了真实交易环境中的收益水平。量化的世界里保守的估计比乐观的幻觉更有价值。你现在跟着这套流程跑出来的结果大概率就处于“看起来还行但远非暴利”的区间这是正常的真正的Alpha往往是在后续持续的因子挖掘和精细打磨中慢慢积累出来的。如果你跑通了代码、想在这个基础版本上继续深入我建议下一个阶段重点做两件事第一把单因子分析报告模板化每加入一个新因子就用IC曲线、分组回测和相关性热力图这三个维度体检一遍第二引入行业分类做行业中性化处理防止策略的表现被一两个行业的集中暴露主导。完成这两步之后你的模型就已经具备了一个小型量化团队的研究雏形。