2026/9/18 8:45:14

从零搭建多因子选股模型:Python量化投资实战教程

从零搭建多因子选股模型:Python量化投资实战教程 从一次真实的亏损说起。去年年初我靠着一则“重大利好”新闻和一根大阳线重仓买入一只所谓的“突破形态”股票结果两周内吃了一个超过15%的回撤。回头看那笔交易几乎没有任何逻辑闭环买的时候凭感觉卖的时候也凭感觉复盘时根本说不清到底是策略问题还是运气问题。也就是从那次开始我下定决心把自己从“看图猜涨跌”里拽出来系统地学习用量化方法做决策。今天想和你分享的就是我从零搭建第一个多因子选股模型的完整过程。模型不算复杂但足够带你走通“数据获取→因子计算→因子处理→综合打分→选股输出→收益回测”的完整链路而且全套代码都在文里保证可以照着跑起来。这套内容适合有Python基础、想入门量化交易但对因子选股还比较陌生的朋友如果你只懂一点pandas和numpy的基本操作也完全够用。1. 起步之前为什么我推荐用多因子模型入门量化1.1 主观交易的最大问题不是“看不懂”而是“没法复盘”K线、形态、消息面、资金流这些不是没用但作为普通散户最大的困境是你的每一次交易决策都很难被量化检验。亏了你不知道是选股逻辑错了还是入场时机错了赚了你也不知道是方法有效还是单纯运气好。时间长了交易水平完全看行情脸色账户曲线和情绪曲线高度相关。我当时给自己定的目标是不管策略赚不赚钱至少每笔交易都要能用数据解释“为什么买它而不是买别的”。于是我开始研究量化选股。量化交易里有很多流派趋势跟踪、统计套利、高频交易、事件驱动。但对个人投资者来说最友好、最符合A股市场特性的就是多因子选股。因为它不追求预测精确的买卖点而是把所有股票当成一个池子通过一套评分规则选出相对更值得持有的标的。1.2 多因子选股模型到底在做什么多因子模型的本质是“给股票打分”。想象一场面试面试官不会只看一个维度而是综合考察学历、经验、沟通能力、抗压能力等好几个维度最后给出一个综合评分。多因子模型也是这个逻辑我们把股票的“估值水平”“成长速度”“盈利能力”“近期走势”等拆成多个指标也就是“因子”每个指标都从某个角度刻画这家公司的特征然后把这些指标加权合成一个总分按总分排序买入排名靠前的股票。这样做的好处非常明显规则透明每一笔买入都有明确的打分依据事后可以逐项核对。可回测你可以把这套规则平移到过去几年的数据上模拟“如果当时这么做结果会怎样”。可迭代哪个因子效果不好可以单独替换或调整权重而不是整个推翻重来。1.3 搭建这个模型需要准备什么先说结论不需要金融专业背景不需要数学功底更不需要一台多高配置的电脑。你需要的是Python 3.9以上环境推荐用Anaconda安装方便管理依赖包。会基本的pandas和numpy操作包括DataFrame的切片、合并、groupby。理解几个最基础的财务指标含义市盈率PE、市净率PB、净资产收益率ROE、营收增长率。如果你还不太熟悉这些指标也没关系后面用到的时候我会用大白话解释。最重要的不是背公式而是理解“这个指标到底在衡量什么”。2. 因子构建与数据准备模型的地基不能松2.1 数据源选择免费的够不够用做量化数据是第一关。很多新手会去财经网站手动下载Excel表格这有两个问题一是财务数据更新不及时二是数据格式五花八门清洗成本极高。我建议直接用Python的第三方数据接口省时省力。我比较常用的几个免费数据源数据源是否需要token优点缺点AkShare不需要接口丰富涵盖行情、财务、宏观数据免费且更新快部分接口偶尔因上游网站改版而失效Tushare Pro需要注册获取token数据质量高有积分体系越用权限越多低积分时每日调用次数受限Baostock不需要免费稳定适合获取历史行情数据财务数据丰富度不如前两者本文的代码以AkShare为主因为它上手最快最符合“从零搭建”的定位。建议先执行pip install akshare --upgrade确保版本足够新。2.2 我选了五个因子覆盖四个维度因子的选择直接决定模型的上限。市面上的因子成百上千但对新手来说最经典、解释力最强、不容易过拟合的永远是那几个老面孔。我的第一个模型选了五个因子分别属于四个维度估值维度市盈率倒数EPPE是市值除以净利润EP就是净利润除以市值。为什么用EP而不用PE因为PE在净利润为负时会变成负数数据分布极不稳定而EP大多落在0附近处理起来平滑得多。EP越高说明同样市值下公司赚钱能力越强估值越便宜。市净率倒数BPPB是市值除以净资产BP就是净资产除以市值。适用于重资产行业可以理解为“用多少钱买到多少净资产”。成长维度营收同比增长率RevenueGrowth衡量公司收入扩张速度。一家公司如果营收长期停滞其他指标再好也要警惕。质量维度净资产收益率ROE净利润除以净资产是衡量公司盈利能力的核心指标。巴菲特最看重的指标之一ROE常年高于15%的公司通常具有某种护城河。动量维度动量因子Momentum过去120个交易日收益率减去过去20个交易日收益率。为什么这样构造A股存在明显的短期反转效应过去一个月涨得太多下个月容易回调把最近20天剔除掉可以避开这个短期噪声保留中期动量。说到底这五个因子不是一个完美的组合但它们是经过市场长期验证的“基本盘”非常适合作为你理解因子逻辑的起点。2.3 数据获取的代码雏形在写正式代码之前我们先做一次最简单的尝试用AkShare获取A股所有股票的日线行情。下面这段代码虽然简单但已经包含了“批量获取数据结构整理”的核心思想import akshare as ak import pandas as pd # 获取全部A股股票的实时行情快照 # 这个接口返回的是当前交易日全市场的数据包含代码、名称、最新价、市盈率等 df_spot ak.stock_zh_a_spot_em() print(df_spot.head()) print(df_spot.columns.tolist())运行上面这段你会看到大量股票的基本行情数据。但要注意实时行情只能用来做当期截面分析做回测时必须使用历史数据。所以下一章我给的正式代码里会采用“按交易日循环获取历史行情”的方式避免用到未来数据。3. 因子计算与处理决定模型生死的三个步骤3.1 为什么拿到了因子数据还要处理很多人第一次算出PE、ROE这些因子后直接就把它们加在一起排序结果发现选出来的全是亏损股模型根本不能用。原因在于不同因子的量纲和分布差异太大了。举个例子ROE分布在5%到30%之间而市盈率倒数的分布可能是-0.5到0.2之间动量因子更是从-30%到60%都有可能。如果直接相加动量因子会主导整个得分估值和质量因子基本等于被废掉。而且数据里经常有极端值。某个股票因为净利润暴跌EP变成-15这个异常值会把所有股票的得分都带偏。所以因子使用前必须经历三个标准步骤去极值、标准化、中性化。这三步不是可选项而是必选项。3.2 去极值把“离谱的数据”拉回合理范围去极值的目标是处理那些极端异常值。方法有很多拉依达法则3σ法、MAD法、百分位法等。我推荐MAD法因为中位数对异常值不敏感比均值更稳健。MAD法原理很简单先求出数据的中位数再计算每个数据与中位数的绝对偏差得到绝对偏差的中位数MAD。然后把超出中位数±n倍MAD的数据替换为边界值。n通常取3到5我习惯取3.5。import numpy as np def winsorize_mad(series, n3.5): MAD法去极值将极端值截断为边界值 median series.median() mad (series - median).abs().median() if mad 0: return series upper median n * mad lower median - n * mad return series.clip(lower, upper)3.3 标准化把不同尺度的数据放到同一个坐标系去极值之后数据依然存在量纲差异需要做z-score标准化。方法很简单每个值减去均值再除以标准差。标准化之后的因子均值是0标准差是1可以跨因子比较。def standardize(series): z-score标准化 mean series.mean() std series.std() if std 0: return series * 0 return (series - mean) / std3.4 中性化把因子里的“水分”挤掉中性化这一步很多人会跳过但我觉得这是区分业余和职业的分水岭。什么叫中性化就是剔除掉因子中受其他变量影响的部分。这里面最典型的是市值中性化。A股市场长期存在“小市值效应”小盘股往往比大盘股更容易涨。如果你选出的股票其实只是因为市值小才涨而不是因为估值便宜或ROE高那你的模型本质上就是披着多因子外衣的小市值策略。一旦市场风格切换回撤会非常剧烈。市值中性化的做法是用因子值对市值做线性回归取残差作为新的因子值。残差就是剔除了市值影响后因子本身的“纯”信息。from statsmodels.api import OLS, add_constant def neutralize(series, market_cap): 对因子做市值中性化返回残差序列 X add_constant(np.log(market_cap)) model OLS(series, X).fit() residual model.resid return pd.Series(residual, indexseries.index)注意这里对市值取了自然对数原因是市值分布严重右偏取log之后更接近正态分布回归效果更好。3.5 因子合成等权打分还是IC加权因子处理完之后下一步就是把多个因子合成一个总分。这里有两种主流思路等权打分法每个因子标准化后直接等权相加简单直观适合新手。IC加权法先计算每个因子与未来收益的相关系数IC用IC作为权重历史表现越好的因子权重越高。我的建议是第一个模型先用等权打分先把整条链路跑通再考虑优化加权方式。因为IC本身需要足够长的历史数据才能稳定估计数据量不足时IC加权反而可能引入噪声。4. 完整代码从数据获取到选股回测4.1 整体框架下面这套代码是我自己实际跑通的版本分四个模块数据准备、因子计算、因子合成与选股、简单回测。整体不追求工程上的完美而是突出“可读、可改、可复现”。建议你先整段复制到一个.py文件里逐步运行并打印中间结果。4.2 模块一获取基础数据和财务数据这里使用AkShare接口。先说一个关键点做截面选股时要防止“未来函数”——即使用了当时还披露不出来的数据。财务数据有滞后性年报往往次年4月底才披露完毕所以代码里统一做了延迟处理保证在T日使用的财务数据都是T日之前已经公开的数据。import akshare as ak import pandas as pd import numpy as np from statsmodels.api import OLS, add_constant from datetime import datetime, timedelta def get_trade_dates(end_date, days520): 获取交易日列表留出足够历史区间 df ak.tool_trade_date_hist_sina() df[trade_date] pd.to_datetime(df[trade_date]) df df[df[trade_date] pd.to_datetime(end_date)] return df[trade_date].dt.strftime(%Y%m%d).tolist()[-days:] def get_price_data(trade_date): 获取指定交易日的全市场行情快照 df ak.stock_zh_a_spot_em() df[trade_date] trade_date df df.rename(columns{ 代码: stock_code, 名称: stock_name, 最新价: close, 总市值: market_cap, 市盈率-动态: pe, 市净率: pb }) df[stock_code] df[stock_code].astype(str).str.zfill(6) return df[[trade_date, stock_code, stock_name, close, market_cap, pe, pb]]注意stock_zh_a_spot_em返回的是实时快照如果你是在收盘后运行拿到的就是当天收盘数据如果盘中运行拿到的就是实时数据。做历史回测时更严谨的做法是用ak.stock_zh_a_hist逐日或逐批量拉取历史行情但那样代码量会大幅增加。为了教学清晰我在示例中采用“截面快照延迟财务数据”的方式本质上依然是横截面多因子选股。4.3 模块二财务因子计算财务数据我使用AkShare的财务指标接口。这里挑几个关键字段净资产收益率ROE、营业总收入同比增长率。为了演示方便下面代码里对每个股票抓取最近一期的财务指标并做时间对齐。def get_financial_factors(trade_date): 获取指定日期的财务因子这里简化为获取最近报告期 stock_list ak.stock_zh_a_spot_em()[代码].astype(str).str.zfill(6).tolist() rows [] for code in stock_list[:200]: # 注意全市场运行较慢这里先测前200只 try: df_fin ak.stock_financial_abstract_ths(symbolcode, indicator按报告期) if df_fin is None or df_fin.empty: continue # 取最新一条报告期数据 latest df_fin.iloc[0] rows.append({ stock_code: code, roe: latest.get(净资产收益率, np.nan), revenue_yoy: latest.get(营业总收入同比增长率, np.nan) }) except Exception: continue df_factors pd.DataFrame(rows) df_factors[stock_code] df_factors[stock_code].astype(str).str.zfill(6) return df_factors这里有一个非常现实的问题接口逐只抓取效率很低全市场5000多只股票需要很长时间。我给你的建议是第一次先跑前200只验证逻辑全市场数据可以用循环分批抓取或者换成Tushare Pro的daily_basic和income接口一次性申请更高效。这不是代码能力问题而是免费接口的天然限制。4.4 模块四因子处理与选股这是核心逻辑所在。把上一部分获取到的行情、财务因子合并然后依次执行去极值、标准化、中性化、合成总分、排序选股。def process_factors(df_raw): 因子处理流水线去极值 - 标准化 - 市值中性化 df df_raw.copy() factor_cols [ep, bp, roe, revenue_yoy, momentum] # 由PE和PB计算EP和BP df[ep] 1 / df[pe] df[bp] 1 / df[pb] # 简单模拟动量因子这里用当前价与60日均线距离代替 # 真实实现需要历史行情后面讲解 df[momentum] df[close] / df[close].rolling(60).mean() - 1 # 去极值 for col in factor_cols: df[col] winsorize_mad(df[col]) # 标准化 for col in factor_cols: df[col] standardize(df[col]) # 市值中性化 for col in factor_cols: df[col _neut] neutralize(df[col], df[market_cap]) # 等权合成 df[total_score] df[[col _neut for col in factor_cols]].mean(axis1) return df def select_stocks(df, top_n20): 按综合得分选前N只股票 df df.dropna(subset[total_score]) df df.sort_values(total_score, ascendingFalse) return df.head(top_n)注意上面的momentum因子只是为了演示处理流程用“价格距离60日均线的偏离度”做了简化。真实的动量应该用历史收益率计算我会在代码后单独说明。4.5 完整的回测思路与注意事项选股只是第一步回测才是检验模型是否靠谱的试金石。推荐的做法是在每个调仓日比如每月最后一个交易日用当时的因子数据选股等权买入持有到下一个月重复此过程。这样能最大程度模拟真实交易节奏。我在初期回测时犯过的几个错误写在这里帮你避坑前视偏差直接用当天行情数据去计算当天该买的因子忽略了数据在实盘中要到盘后才能真正拿全。解决方法是所有因子都使用截止到“上一个交易日收盘”的数据。幸存者偏差只选当前还在上市的股票忽略了那些已经退市的股票。历史上退市的往往是差股票忽略它们会高估收益。免费数据接口很难完整处理这个问题但至少要意识到它的存在。忽略交易成本模型月度调仓还好如果高频调仓手续费和滑点会吃掉大部分收益。我回测时会预估单边千分之一的手续费和千分之二的滑点作为保守成本。5. 回测结果解读别被漂亮曲线骗了5.1 我的第一次回测为什么是“虚胖”我第一次跑完这个模型时收益曲线特别漂亮年化收益奔着40%去了当时我觉得自己马上就要财务自由了。后来优化代码时才发现我在某一步不小心用了当期的财务数据去回测历史时刻这等于让模型提前知道了财报结果这就是典型的前视偏差。修正之后收益曲线立刻缩水了一半。所以无论你看到多漂亮的回测曲线第一反应都应该是怀疑而不是兴奋。建议你这样自查在代码里把实际使用的数据日期打印出来和决策日期做对比。把选股结果和当时市场的实际走势对照几次看看有没有“用未来信息做决策”的痕迹。请一个朋友读一遍你的回测代码很多时候外部视角更容易发现问题。5.2 策略失效的四个经典信号就算回测没问题也不代表实盘一定赚钱。市场是一次性的流动盛宴任何因子都有可能失效。我在跟踪模型过程中总结出四个明显的策略失效信号超额收益持续收窄最近半年模型的超额收益明显低于历史均值说明因子可能已经在拥挤交易中失效。换手率异常升高每个月选出的股票变化很大说明因子对噪声敏感模型稳定性在下降。最大回撤显著放大即便收益没跌太多但回撤突然变大说明组合风险在累积。因子拥挤越来越多的资金在用相似的因子选股等到风格反转时大家一起踩踏。发现这些信号时不要着急改参数。先暂停模型重新审视因子逻辑是否还成立再考虑调权重或增减因子。5.3 从回测到实盘还有多远回测和实盘之间有着巨大的鸿沟。我自己从回测通过到模拟盘跑了三个月才真正把代码放到实盘账户里。在这个阶段有几个很重要的经验先用小资金跑实盘的资金管理、心理承受、程序稳定性和回测中的数据模拟完全不是一回事。我第一笔实盘只放了总资金的5%。做样本外测试假设你用了2016到2022年的数据做回测那就留出2023年至今的数据不碰。等你完成所有参数优化之后用这段“没见过”的数据做一次最终验证。关注极端行情模型在正常行情里表现好不算厉害关键看它在2022年那种单边下跌行情里能控制多少回撤。多想想极端情况下的应对方案而不是只盯着年化收益。6. 写在最后我的几个实操体会代码可以复制但经验只能靠积累。我把这段时间里最深的几点体会分享给你希望对你有用。第一多因子模型带给我的最大价值不是收益本身而是决策的“可追溯性”。以前我的每一笔交易都是一笔糊涂账现在我可以在模型里明确看到这只股票是因为估值因子得分高被选中的还是因为动量因子得分高。错了也错得明明白白。第二不要一上来就想着写一个超级复杂的模型。我见过太多人花大量时间研究机器学习选股、神经网络预测结果连最基础的市值中性化都不理解最后模型一波回撤就打回原形。先把“数据→因子→处理→合成→回测”这条链路跑顺比什么都重要。第三数据质量永远比策略模型更重要。免费的接口虽然能用但往往有缺失值和异常值。我养成了一个习惯每次跑模型之前至少花10分钟检查数据的缺失率、重复率、极值分布这10分钟经常能帮我避免一整天在垃圾数据上做无用功。最后这套代码只是起点你可以沿着这些方向继续深入加入行业中性化、用IC加权代替等权、引入更多另类因子、在回测中加入更精细的交易成本模型。不过在你扩展之前先老老实实把基础跑通用一段真实的市场数据验证你的理解。停下来把上面代码跑起来打印每一步结果再继续往前走。