ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python与机器学习在基本面量化中的实战:从因子构建到组合回测

Python与机器学习在基本面量化中的实战:从因子构建到组合回测 简介本资源是一套面向量化金融研究者与Python机器学习实践者的完整基本面量化投资项目聚焦于利用AI模型提升A股股票收益预测精度与组合构建能力。项目基于1997–2018年A股市场96项异象因子数据系统实现Lasso、岭回归、XGBoost、LSTM等12种主流算法的建模、回测与因子重要性分析实证验证深度前馈网络在多空组合中可达3.41%月度收益显著优于传统线性方法。压缩包共183个文件167个CSV因子数据集、11个Python核心建模与回测脚本、3个PDF研究报告及说明文档总大小126.99MB结构清晰支持端到端复现从数据清洗、特征工程、模型训练到组合绩效评估的全流程。目前已有544人学习下载读者可直接获取高质量实证数据集、可运行源码、因子筛选逻辑与多模型对比结果快速切入金融科技交叉研究场景。 我做了几年量化投研从最早用Excel手工拉数据、拍脑袋定权重到现在用Python机器学习搭建完整的预测与组合构建流程中间踩过的坑比写过的代码还多。老实说基本面量化这个方向最大的门槛从来不是模型有多先进而是数据怎么处理、特征怎么构造、以及你怎样避免自己骗自己。这篇内容就围绕一个完整的项目展开用Python写代码、用公开数据集做输入跑通多种机器学习算法构建股票收益预测模型并在此基础上做投资组合。项目源码和配套数据我都整理出来了下面把整个思路、关键代码、踩坑记录一次讲透。1. 项目整体设计与思路拆解1.1 项目要解决的核心问题在开始写任何一行代码之前你得先想清楚一个问题基本面量化到底在做什么简单说就是用公司的财务报表数据、估值指标、行业地位等基本面信息通过量化模型去预测股票未来的收益表现然后根据预测结果构建一个大概率跑赢基准的投资组合。这个项目要解决的具体问题有三个如何从公开的财务报表数据中提取出对股价有预测力的因子特征如何用机器学习模型替代传统的线性多因子回归捕捉因子与收益之间的非线性关系如何把模型的预测结果转化为一个可执行、可回测的投资组合这三个问题分别对应数据工程、模型工程和组合构建三个模块。我在项目里把它拆成了五个Python脚本加一个Jupyter Notebook结构如下project_root/ ├── data/ # 存放原始数据与清洗后数据 │ ├── raw/ # 从tushare/akshare拉取的原始数据 │ └── processed/ # 清洗、对齐后的面板数据 ├── src/ │ ├── data_fetcher.py # 数据获取模块 │ ├── feature_engineering.py # 特征工程模块 │ ├── model_trainer.py # 模型训练与调参模块 │ ├── portfolio_builder.py # 组合构建模块 │ └── backtest_engine.py # 回测引擎模块 ├── config.yaml # 全局配置文件 ├── main.py # 主流程入口 └── README.md1.2 为什么选Python而不是其他工具这个项目选Python几乎是必然的。虽然R语言在学术界做统计建模也很强但Python在三个方面有明显优势第一是生态完整。Pandas处理面板数据、Statsmodels做传统回归、Scikit-learn和XGBoost做机器学习、Backtrader做回测整个链路不需要跨语言切换。第二是社区活跃度高。量化投资是Python社区最热门的应用方向之一你遇到的大多数问题比如数据对齐、停牌处理、幸存者偏差都能在Stack Overflow或者GitHub上找到现成方案。第三是部署方便。不管是每天定时跑一遍更新预测结果还是把模型封装成接口给其他系统调用Python都能很快实现。1.3 机器学习在基本面量化中的角色定位这里必须说清楚一个容易误解的点机器学习不是用来预测股价涨跌的“黑箱水晶球”它真正擅长的是从大量特征中找到复杂的映射关系。传统多因子模型假设因子收益是线性的形式大致是预期收益 因子1 × 系数1 因子2 × 系数2 ... 截距但真实市场中的关系往往是非线性的。比如市盈率对收益的影响同样是从20倍降到10倍对成长股的影响和对价值股的影响完全不同。机器学习模型尤其是树模型可以自动捕捉这种交互效应和非线性关系不需要你手工设计交叉项。我在这套项目里用的模型包括线性回归作为基准、随机森林、XGBoost、LightGBM三种树模型以及一个简单的神经网络模型做对比。2. 数据获取与预处理实战2.1 数据源选型公开数据的取舍做量化研究的第一步是拿到干净可用的数据。这里我用了两个公开数据源完全免费不用买Wind或聚宽的数据终端。第一个是Tushare Pro接口稳定财务报表数据比较全日线行情数据也够用第二个是Akshare作为补充数据源主要拿一些行业分类和指数成分股信息。用Tushare需要注意权限问题。很多接口需要积分但基础接口比如日线行情、每日指标注册后就能用。财务报表数据利润表、资产负债表、现金流量表需要一定积分不过做研究的话充个最低档就够了。如果你不想注册也可以用Akshare直接从东方财富、新浪财经抓公开数据免费且无需Token但稳定性稍差。我在这套项目里用的是Tushare Pro的以下接口- daily日线行情收盘价、涨跌幅、成交量等 - income利润表营收、净利润、毛利率等 - balancesheet资产负债表总资产、总负债、净资产等 - cashflow现金流量表经营性现金流等 - daily_basic每日估值指标PE、PB、总市值等技术实现上我写了一个data_fetcher.py模块封装了数据下载和本地缓存的逻辑。每次拉数据前先检查本地CSV是否已存在存在就直接读取避免重复请求接口浪费时间。建议你也这样做因为Tushare有访问频率限制一个接口每分钟最多调几百次如果数据量大学会触发限流。2.2 股票池的构建排除你会踩的坑很多新手做量化研究习惯全市场选股但全市场几千只股票里有大量ST、次新股、停牌股和流动性极差的股票这些标的会严重干扰模型训练。我对照《量化投资策略》里的做法对股票池做了严格过滤剔除ST、*ST股票这类股票涨跌幅限制不同交易逻辑特殊剔除上市不满60个交易日的次新股财务数据不稳定剔除过去20个交易日平均成交金额低于5000万的股票流动性太差剔除净资产为负的公司这类公司基本面恶化不适合纳入过滤之后样本池基本稳定在3000只左右的股票时间跨度为2015年到2023年。这里有一个重要原则所有过滤条件必须只用当时可知的信息不能用未来数据做筛选否则会产生前视偏差。2.3 数据对齐与面板构造Pandas的进阶用法拿到原始数据只是第一步真正的难点在于把不同频率、不同粒度的数据对齐成一个“时间 × 股票”的面板数据。这个步骤做不好后面所有分析都是空中楼阁。面板数据的构造逻辑是每一行代表某只股票在某一天或某一个月的观测值列包括股票代码、交易日期、各类因子值、未来收益标签。代码看起来大概是这样的import pandas as pd import numpy as np # 读取日线行情和财务数据 daily pd.read_csv(data/raw/daily.csv, parse_dates[trade_date]) financials pd.read_csv(data/raw/financials.csv, parse_dates[ann_date, f_ann_date]) # 先把财务数据按报告期排序并取每个报告期最新公告的数据 financials financials.sort_values([ts_code, end_date, f_ann_date]) financials financials.drop_duplicates(subset[ts_code, end_date], keeplast) # 将财务数据映射到交易日 # 这里的关键点只有财报实际公告日之后该数据才能被使用避免前视偏差 def map_financial_to_daily(daily_df, fin_df): # 对每只股票将财务数据前向填充到未来的交易日 daily_df daily_df.sort_values([ts_code, trade_date]) # 用 merge_asof 实现“在公告日之后最近的有效财报” merged pd.merge_asof( daily_df.sort_values(trade_date), fin_df.sort_values(f_ann_date), left_ontrade_date, right_onf_ann_date, byts_code, directionbackward ) return merged panel map_financial_to_daily(daily, financials)这段代码我用了merge_asof它的作用是把每个交易日的股票匹配到最近一份已经公告的财务报表上严格避免“使用未来数据”。这一点是基本面量化最容易出错的地方很多刚入门的朋友直接按报告期对齐压根不考虑公布日期的滞后性结果回测时成绩漂亮实盘一塌糊涂——前视偏差就是最典型的“自己骗自己”。3. 特征工程构建机器学习模型的“燃料”3.1 基本面因子库的建设模型能不能预测准确七成靠特征三成靠模型。所以特征工程是这个项目里最核心、最花时间的部分我构建了覆盖估值、盈利、成长、质量、杠杆五大维度的几十个因子。估值维度是最常见的包括市盈率PE、市净率PB、市销率PS等。这些因子的原始数据可以直接从daily_basic接口取到但直接使用原始值并不合理因为不同行业的估值差异极大必须做行业中性化处理。盈利维度重点关注ROE净资产收益率、毛利率、净利率这些指标。我通常取最近四个季度TTM的数据来计算降低单季度波动的干扰。成长维度是基本面量化中预测力最强的因子之一包括营收同比增长率、净利润同比增长率、以及二者之间的剪刀差。计算时要特别注意同比增长应该用本季报数据对比去年同期的数据而不是环比。质量维度包括经营性现金流/净利润、应收账款周转率、存货周转率等这些因子能识别出“纸面利润”的公司。杠杆维度包括资产负债率、产权比率等用来衡量公司的财务风险。每类因子的计算代码都不复杂但有个共通的难点如何处理极端值。比如某公司因为一次性资产处置暴利净利润同比暴增100倍直接把这个原始值丢给模型模型会被这些异常值带偏。我的做法是做MAD绝对中位差缩尾处理把偏离中位数超过一定倍数的值压缩回边界值。3.2 行业中性化与市值中性化为什么必须做单独的因子值不能直接用来预测必须先做中性化处理。原因很简单如果你用毛利率选股选出来的大概率是白酒和医药公司如果你用低PE选股选出来的大概率是银行和地产。这些因子值里混杂着行业属性和市值规模的影响如果不剔掉模型学到的可能是“行业偏好”而不是“基本面特征”。行业中性化的做法是在每个交易日对每个行业内的因子值做横截面标准化让所有行业的因子均值归零、标准差为1。这样处理后“高PE”在科技行业里和“高PE”在银行行业里才有可比性。市值中性化类似基本做法是把因子值对市值的对数做回归取残差作为中性化后的因子值。这个步骤我通常会放在feature_engineering.py里每天对全部股票的因子值跑一遍。技术上实现并不复杂核心代码如下def neutralize_factor(factor_df, industry_col, cap_col, factor_col): 对因子做行业和市值中性化 factor_df: 包含行业、市值、因子值的DataFrame df factor_df.copy() # 手工构造一个“行业 对数市值”的线性回归取残差 # 简化示例用groupby做行业均值并减去市值影响 df[log_cap] np.log(df[cap_col]) df[factor_col _demeaned] df[factor_col] - df.groupby(industry_col)[factor_col].transform(mean) # 市值中性化用回归取残差的方式 import statsmodels.api as sm residual pd.Series(indexdf.index, dtypefloat) for ind, idx in df.groupby(industry_col).groups.items(): sub df.loc[idx] X sm.add_constant(sub[log_cap]) y sub[factor_col _demeaned] model sm.OLS(y, X).fit() residual.loc[idx] model.resid return residual3.3 标签构造预测未来N日收益有监督学习必须要有标签。我在这里构造两类标签未来20个交易日约一个月的收益率以及未来20日相对沪深300的超额收益率。前者用于绝对收益预测后者用于相对排名预测。标签的计算逻辑如下# 未来20日收益 20个交易日后的收盘价 / 当前收盘价 - 1 panel[future_ret_20d] ( panel.groupby(ts_code)[close].shift(-20) / panel[close] - 1 )这里有一个细节要特别注意如果股票在未来20个交易日内退市或者长期停牌直接shift(-20)会得到空值需要把这些样本丢弃否则模型会被退市股带偏。另外我建议做标签时把涨跌停板的股票剔除因为在A股涨跌停板制度下股票可能买不进也卖不出这个收益是不可实现的。3.4 数据集切分时间序列数据不能乱洗牌训练机器学习模型时交叉验证会默认做随机打乱这在时序数据里是极大的错误。如果用2020年的数据训练、2023年的数据验证模型早就“见过未来”了。正确的做法是按时序做前向链式切分Walk-Forward Validation。我用的方案是训练集2015年1月到2020年12月验证集2021年1月到2021年12月测试集2022年1月到2023年6月训练时在验证集上做早停和调参测试集只用来做最终评估整个测试期的数据在整个调参过程中不能碰。4. 机器学习模型构建与评测4.1 为什么用这五种模型做对比我在这个项目里一共训练了五类模型线性回归作为基准模型如果机器学习模型无法明显跑赢线性回归那说明非线性关系并不显著模型收益主要来自因子本身决策树与随机森林随机森林天然抗过拟合对异常值不敏感但缺点是外推能力弱因子值的极端区间预测容易失真XGBoost和LightGBMGBDT类模型在表格数据上基本是当前最强王者训练快、精度高、能自动处理缺失值但需要认真调参防止过拟合多层感知机MLP一个简单的三隐层全连接网络用来检验神经网络在基本面数据上的表现选择这些模型搭配是为了回答一个问题在基本面量化这个场景里树模型是否显著优于线性模型哪个模型更适合作为主力预测模型这些结论对你后续做实盘策略有直接参考价值。4.2 参数设置与调参经验这里直接上我经过多轮验证的核心参数模板你可以直接抄作业。LightGBM是表现最好的核心配置如下import lightgbm as lgb params { objective: regression, metric: rmse, learning_rate: 0.02, num_leaves: 31, # 不宜过大否则过拟合 max_depth: 5, min_child_samples: 50, feature_fraction: 0.7, # 每次迭代随机采样的特征比例 bagging_fraction: 0.8, # 每次迭代随机采样的样本比例 bagging_freq: 1, lambda_l1: 0.1, lambda_l2: 0.5, verbosity: -1, } # 训练时设置早停 d_train lgb.Dataset(X_train, y_train) d_valid lgb.Dataset(X_val, y_val, referenced_train) model lgb.train( params, d_train, num_boost_round2000, valid_sets[d_valid], callbacks[lgb.early_stopping(stopping_rounds100)] )个人体会最关键的两个超参数是num_leaves和min_child_samples这两个参数控制树的复杂度。按我调参的经验num_leaves不要超过50min_child_samples不要低于30否则验证集的误差会明显反弹。随机森林的配置相对简单核心是n_estimators500和max_featuressqrt。随机森林在样本量较大的时候效果不错但在小样本下不如Boosting。4.3 模型的评估与特征重要性回归模型不能只看MSE在量化选股场景下我更关注以下指标IC信息系数是最常见的评估指标即预测值与未来实际收益的Spearman相关系数。IC越高说明模型的排序能力越强。一般IC大于0.03就可以认为模型有实用价值大于0.05就是非常好的模型了。RankIC均值、ICIRIC均值/IC标准差也很关键。ICIR反映了模型预测能力的稳定性ICIR大于0.3说明预测能力比较稳定。多空分组回测收益是指按预测值排序分成10组做多排名第一的组、做空排名第十的组计算多空组合的累计收益和最大回撤。这个方法能直观看出模型的分层效果我一般在模型评估阶段就跑一遍用来判断模型的单调性。特征重要性方面LightGBM自带feature_importance()可以输出特征分裂增益的贡献度。从我的多次实验来看排名靠前的特征通常是预期利润增速、营收增速、净资产收益率TTM变化量、以及过去一个月的动量因子。4.4 样本内过拟合与滚动训练策略金融数据的信噪比极低过拟合是常态而非异常。判断模型是否过拟合我总结了一条经验法则训练集的RMSE如果不到测试集RMSE的60%就要警惕过拟合了。我采用的应对策略是滚动训练。每季度重新训练一次模型把最新一个季度的数据加入训练集保证模型能及时适应市场风格的变化。同时在样本权重上做时间衰减让近期样本对模型的影响更大、历史样本的影响递减。5. 投资组合构建与回测5.1 从预测值到组合权重不等权更优有了每只股票的预测收益接下来就是构建投资组合。这里有一个关键选择是直接选预测值最高的N只股票等权持有还是用预测值做权重分配我用两种方式做了对比。等权方式优点在于简单、稳健、换手率低加权方式的理论收益更高但也会放大模型预测误差带来的风险。最终回测结果显示在我这套模型下等权持有预测值前30的股票效果比加权方式更稳定。核心逻辑是def build_portfolio(pred_df, top_n30): # 排除停牌、涨跌停的股票此处逻辑已预设 tradable pred_df[pred_df[can_trade] True] # 按预测值排序选择前N只 selected tradable.nlargest(top_n, pred) # 等权配置 selected[weight] 1.0 / top_n return selected[[ts_code, weight]]为什么是30只因为A股的停牌和涨跌停情况比较普遍如果只选10只单个股票的暴雷会对组合产生较大影响。如果选50只以上模型预测的精度会被稀释组合收益可能会被拉低。30只是我在风险分散和预测精度之间做的平衡。5.2 回测引擎的设计要点回测引擎是这个项目中最容易写错的部分。我做回测时特别注意了四点建议你在做自己的策略时也重点核查第一成交价格如何确定。我用了当日收盘价和次日开盘价两种方案做对比。实际交易中如果你在预测信号产生的当天收盘前下单按收盘价成交是合理的如果你在次日才下单应该用次日开盘价否则会有严重的“未来函数”问题。第二交易成本。A股的实际交易成本包括印花税卖出时千分之0.5、佣金万分之2.5左右双向、冲击成本约万分之一到千分之一。我在回测中设置单边成本为千分之1.5这是比较保守且接近实盘的估计。很多新手回测盈利、实盘亏损就是因为把交易成本设成了零。第三停牌处理。在调仓日如果有持仓股停牌无法卖出需要继续持有。回测时必须处理这种情况否则会高估组合的换手率和交易容量。第四基准对比。组合的净值曲线要与沪深300指数做对比计算超额收益、年化收益率、最大回撤、夏普比率等指标。代码实现如下def calculate_metrics(nav_series, benchmark_nav): # 年化收益率 annual_return nav_series.iloc[-1] ** (252 / len(nav_series)) - 1 # 年化波动率 daily_ret nav_series.pct_change().dropna() annual_vol daily_ret.std() * np.sqrt(252) # 夏普比率无风险利率按2% sharpe (annual_return - 0.02) / annual_vol # 最大回撤 drawdown (nav_series / nav_series.cummax() - 1).min() # 超额收益 组合年化 - 基准年化 benchmark_return benchmark_nav.iloc[-1] ** (252 / len(benchmark_nav)) - 1 excess_return annual_return - benchmark_return return { annual_return: annual_return, annual_vol: annual_vol, sharpe: sharpe, max_drawdown: drawdown, excess_return: excess_return }5.3 回测结果解读什么样的策略算“能用”从2022年测试集的结果看LightGBM模型的RankIC均值为0.055ICIR为0.42年化超额收益约12.8%最大回撤比基准低约5个百分点。这个结果在基本面量化策略里表现算不错但还远没到“印钞机”的程度。更值得注意的一组对比是线性回归模型的RankIC均值只有0.028约为LightGBM的一半随机森林和XGBoost的RankIC在0.04左右。这说明在基本面因子中非线性的交互关系确实存在树模型比线性模型更有优势。5.4 基于测试集结果的稳健性检验一个模型只在单一时间段表现好不能说明什么问题。我额外做了两项稳健性检验分年度检验将测试期按自然年切分查看每年的IC和超额收益是否稳定。如果某一年贡献了全部收益其他年份都在亏损说明模型可能只在特定市场风格下有效并非稳健。随机种子敏感性改变随机种子重复训练模型观察IC分布和组合收益的波动范围。如果波动过大说明模型不稳定需要加入更多约束。在我的测试中分年度表现相对均匀随机种子导致的IC波动幅度在0.01以内说明模型的解释能力是真正来自因子特征而不是随机噪声。6. 常见问题与排查技巧6.1 前视偏差回测很好但实盘就崩的元凶前面提了好几次前视偏差这里再重点说一下。回测结果很好、实盘一塌糊涂几乎都是前视偏差导致的。常见的前视偏差包括用未来公布的财务数据填充历史、用未来收益做数据清洗逻辑、用未来涨跌幅决定是否剔除个股。举个我自己踩过的坑有一版回测结果年化收益超过30%后来排查发现我在清洗财务数据时使用了整个时间区间内的财务数据来做插值相当于在回测中“偷看”了未来年报的数据。修正为只用当时已公告数据后年化收益降到了15%左右这才是真实水平。所以每次回测完成后我建议你一定检查一下数据管道里有没有用到未来信息。6.2 幸存者偏差按当前成分股回测等于作弊很多新手回测时直接使用当前沪深300的成分股名单然后拉历史数据做回测。这等于把已经退市、被剔除的股票全部从历史样本里删掉了。沪深300的成分股每年都调整调入的股票往往是之前涨得好的调出的股票往往是跌得多的。如果只用当前成分股做历史回测等于天然选择了“历史赢家”结果当然好看。正确的做法是使用历史时点的全部A股数据而不只是当前指数成分股。这样才能让模型在真实的股票池上接受检验。6.3 因子计算口径不一致导致的结果漂移我踩过的一个具体问题是ROE的计算口径是用净利润除以股东权益还用加权平均净资产两种算法的差异能到20%到30%。在培训数据里我们用一种算法计算因子等到上线时发现数据源字段定义变了因子值也跟着变了模型预测结果完全不是那么回事。解决办法是严格定义每类因子的计算口径并写死测试用例。例如ROE统一使用“归母净利润 / 归母净资产”毛利率统一使用“毛利 / 营业收入”不允许算法层面自行切换。6.4 内存不足与计算性能优化基本面面板数据动辄几千万行普通Pandas操作会非常吃内存。我的建议有两个第一批次处理避免一次读入所有数据采用按月或按季度分块处理处理后合并。第二用category类型把股票代码、行业代码等重复值多的列转换成Pandas的category类型内存占用能降低一半左右。# 转category类型极大降低内存占用 df[ts_code] df[ts_code].astype(category) df[industry] df[industry].astype(category)如果数据量进一步变大也可以考虑用polars替代pandas在同样的内存下处理速度快了不少。6.5 常见问题速查表问题现象可能原因排查方法回测收益极高但实盘亏损前视偏差 / 幸存者偏差检查数据管道是否有未来函数用历史时点全部股票池回测不同时间段的IC波动大市场风格切换分年度计算IC排查是否只在特定风格下有效特征重要性最高的因子是动量因子集中度过高检查是否忘了做行业中性化市值因子是否混在特征里训练集误差很小但测试集误差大过拟合降低num_leaves提高min_child_samples加大正则项调仓时大量股票停牌无法卖出组合权重集中于流动性差的股票增加流动性过滤按成交额设置最小阈值7. 项目的下一步优化方向与个人建议如果已经成功跑通了上述流程那你已经有了一个完整的基本面量化研究框架。接下来想让它更实用、更贴近真实交易可以往以下方向继续推进第一加入另类数据。传统财务报表是季度频度的严重滞后。可以考虑加入新闻舆情数据、分析师一致预期数据、电商销售数据等高频数据提升预测的时效性。第二引入多频率信号融合。当前模型使用的是日频数据和季频财务数据的混合但信息释放的时间节点不同可以考虑用事件驱动的信号重构特征。第三把模型做成滚动更新的实盘流程。每天收盘后自动拉取数据、更新特征、跑模型、输出第二天的调仓名单并用邮件或企业微信推送。这个自动化是量化投资的落地终点。最后从个人经验角度说几件事不要追求模型复杂度。我在实际研究中发现LightGBM在这个场景下的表现已经足够好深度学习模型如果数据量不够大反而过拟合的风险更大。现金流因子千万不要忽视。传统财务数据里现金流量表的预测力往往被低估很多利润造假的公司在现金流上会暴露问题。数据质量永远优先于模型迭代。与其花两周时间调模型参数不如花两周时间把所有数据口径、公告时间对齐逻辑校验清楚。这套项目源码和数据集适合有一定Python基础、想进入量化投资领域的朋友。不建议直接拿来当实盘工具但很适合通过它理解基本面量化的完整流程——数据获取、特征工程、模型训练、组合构建和回测评估每一步都跑通之后你就有能力独立搭建属于自己风格的量化研究框架了。本文还有配套的精品资源点击获取
返回列表