ARTICLE DETAIL

资讯详情

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

基于Python的机器学习基本面量化投资系统构建全解析

基于Python的机器学习基本面量化投资系统构建全解析 简介基本面分析与量化投资的结合是近年来的热门方向。传统多因子模型受线性假设、因子共线性和财报滞后等问题困扰而机器学习特别是集成树模型如LightGBM能够更好地捕捉非线性关系并在低信噪比财务数据中构建稳健的预测模型。然而真正的难点在于数据管道财报日期对齐、截面标准化、行业中性化以及未来函数的规避直接决定模型是否可信。从收益预测到投资组合优化再到回测中幸存者偏差、交易成本等隐性陷阱每一个环节都影响实盘效果。整套方案基于Python完整实现系统梳理了从财报数据抓取清洗到机器学习建模、组合优化及回测验证的全流程帮助读者避开常见坑点搭建可落地的基本面量化投资系统。 我一直觉得基本面投资和机器学习天生就该走到一起但真正把两者完整揉进一套系统里的人远比想象中少。两年前我开始动手搭建一套基于Python的机器学习驱动的基本面量化投资系统从财务报表数据的抓取清洗到构建股票收益预测模型再到投资组合的权重分配前后迭代了快三个版本。最近终于把整套源码和脱敏后的数据集整理出来趁着整理项目的机会我把整个链路里最容易卡住的地方和踩过的坑一次性梳理清楚。这篇文章适合那些已经会用Python做数据分析但对量化投资落地路径还比较模糊的读者——尤其是想知道机器学习模型在基本面选股里到底该怎么用、回测结果为什么好看但实盘掉链子的人。源码和数据集的复现方式我会放在最后中途讲原理和坑的地方建议你对着自己的数据试一遍。1. 为什么基本面选股需要机器学习传统多因子模型的三个硬伤先聊清楚一个问题既然用市盈率、ROE这些财务指标选股已经做了几十年凭什么觉得机器学习能做得更好这个问题的答案藏在传统多因子模型三个绕不开的硬伤里。1.1 线性假设太强收益和因子往往不是直线关系传统多因子模型的基本形式是预期收益等于因子暴露乘因子收益再加残差核心工具是线性回归。这里最大的问题不是回归本身而是它默认因子和未来收益之间是一条直线。但真实市场里这种线性关系经常只在某个区间成立。我举个简单的例子账面市值比这个因子朴素的线性模型认为它越高越好体现为一条单调上升的直线。可实际数据里小市值区间和超大盘区间的斜率完全不一样中间甚至还有平台期。机器学习里的树模型天然就能切出这种分段关系XGBoost、LightGBM这类带正则的集成模型在信噪比很低的财务数据里远比分裸的线性回归稳定。1.2 因子拥挤与多重共线性回归系数极不稳定基本面因子几十个相关性非常强。比如ROE高的公司毛利率、净利率、周转率往往也高把这些高度相关的变量丢进OLS系数估计会变得极其不稳定今天跑出来是这个系数明天多加一个因子可能整个符号就变了。机器学习模型其实也有共线性问题但集成树对特征间冗余的容忍度高很多而且可以通过特征重要性分析把真正有效的维度挑出来。1.3 财报天然滞后数据管道弥漫着未来函数财务数据本身就是低频、滞后的。年报最晚到次年4月底才披露半年报要等三季度如果直接用“会计期末日期”对齐市场数据模型在训练时就会“偷看”未来回测结果虚高得离谱。这个问题不是调参能解决的必须从数据管道上就堵住。正确做法是用公告日期对齐而不是报告期。这块在第二章会仔细展开。1.4 机器学习补位的两条路线回归还是排序机器学习的引入路径主要分两条。一种是回归路线用特征直接预测未来20日收益率得到一个连续的预测值另一种是排序路线预测股票之间的相对优劣典型方法有RankNet、LambdaRank这类排序学习模型。我在这套项目里选择的是以LightGBM回归为主模型分类和排序作为对照验证的方案。原因很简单回归模型的分数可以直接被组合优化模块使用评估指标RankIC和选股逻辑衔接也自然。2. 数据清洗与特征工程决定模型上限的往往是这一步很多人拿到“源码数据集”第一反应是去看模型效果实际上这套项目里最容易出错、也最影响最终收益的是数据管道。机器学习圈有句话叫“垃圾进垃圾出”在量化里应该改成“未来函数进虚拟收益出”。2.1 财报数据对齐用“公告日期”从源头消灭未来函数量化研究里有个词叫未来函数指模型在某个时间点上提前使用了那时还拿不到的信息。基本面数据里最典型的未来函数就是财报日期错位。数据表里通常有两列report_date报告期和 announce_date实际公告日期。如果你在2023年5月1日做决策能用的只能是 announce_date 小于等于2023年5月1日的最新一期财报而不是 report_date 在2023年3月31日之前的报表因为那份年报可能到4月28日才披露。import pandas as pd df pd.read_csv(fundamental_data.csv) df[announce_date] pd.to_datetime(df[announce_date]) df[report_date] pd.to_datetime(df[report_date]) df df.sort_values([stock_code, announce_date]) # 对每个交易日找到该股票当前最新一期已公告的财报 trade_dates pd.read_csv(trade_dates.csv, parse_dates[trade_date]) def get_latest_fundamental(stock_df, trade_date): mask stock_df[announce_date] trade_date latest stock_df.loc[mask].sort_values(report_date).tail(1) return latest.iloc[0] if not latest.empty else None这只是一个示意真正工程化时会用 SQL 的 asof join 或者 pandas.merge_asof效率会高很多。这个对齐逻辑就是整个项目数据管道的地基如果这一层错了后面所有模型和回测都是白搭。2.2 特征标准化截面Z-Score比时序归一化更适合基本面数据财务指标量纲差异太大营收几十亿增长率只有0.1PE可以到几百。树模型对量纲不敏感但标准化仍然必要尤其是你要做模型对比或者之后换神经网络的时候。标准化最容易踩的坑是用了全样本均值方差很多人直接对整个数据集做StandardScaler这在机器学习常规任务里没问题但放在时序数据里就成了作弊标准化时已经偷看了未来所有样本的均值和方差。正确的做法是在每个交易日对全部股票做截面标准化也叫截面Z-Score。factor_cols [roe, gross_profit_margin, pe, pb, revenue_yoy] def winsorize_and_standardize(df, factor_cols): # 截面去极值用MAD截尾 for col in factor_cols: median df[col].median() mad (df[col] - median).abs().median() lower median - 5 * (1.4826 * mad) upper median 5 * (1.4826 * mad) df[col] df[col].clip(lower, upper) # 截面标准化 df[factor_cols] df.groupby(trade_date)[factor_cols].transform( lambda x: (x - x.mean()) / x.std() ) return df这里我用MAD截尾而不是简单的百分位截尾MAD对离群值更稳健。1.4826是把MAD调整为和标准差等价的一个常数在财务数据这种长尾分布里非常好用。你如果直接看原始PE分布会发现个别股票能到几百倍但大部分集中在10到40之间不用MAD截尾的话极值会主导标准化结果。2.3 派生特征与行业中性化处理原始财务字段直接进模型效果一般真正有价值的是派生特征。项目里我常用的有这么几类盈利能力ROE、毛利率、净利率、经营现金流/净利润成长性营收同比、净利润同比、毛利率环比变化营运效率存货周转率、应收账款周转率、总资产周转率估值PE、PB、PS、股息率、PEG财务健康资产负债率、流动比率、利息保障倍数这些特征组合出来的意义在于呈现“质量”与“估值”的权衡关系这也是基本面投资的基本逻辑。比如一个ROE很高但资产负债率过高的公司可能会在财务健康维度上被拉回来一个PE很低但成长性为负的公司也不会天生分数就高。行业中性化的处理也放在特征层做完截面Z-Score之后再对每个行业分组减去行业均值。比如同一只银行股它的PE在全市场可能极低但放在银行板块里未必是低估如果直接全市场标准化模型容易选出一堆PE低的银行股本质上是在赌行业而不是赌个股。2.4 数据集划分的边界线在哪里做监督学习数据划分是另一个暗坑。财务数据是时序数据绝对不能随机打乱做K-Fold必须按时间划分。我建议训练集、验证集、测试集的比例大致是6:2:2按年份切例如2016-2018训练2019-2020验证2021-2023测试。测试区间最好包含牛熊转换这样能看出模型在不同市场环境下的表现。标签生成也容易犯错。如果你要预测未来20日收益率标签等于未来第20个交易日的收盘价除以今日收盘价减1。这里同样要确保这些未来价格在回测时是当前不可知的。一个通用的原则是任何在时点t生成的字段只能使用t时刻之前已公开的信息。3. 收益预测模型选型回归、分类还是排序数据管道处理好之后进入模型层。这里第一个问题不是选哪个库而是选哪种建模范式它会直接决定后续所有评估和组合逻辑。3.1 三种建模范式的取舍对比回归模型。目标y是未来20日收益率优点是直接、可解释性好预测值可以和组合优化直接衔接。缺点是收益分布长尾极值对损失函数影响大所以训练时通常会对y做缩尾处理。分类模型。把y大于0标记为1否则为0用Logistic回归或者树模型做分类。优点是预测任务更简单、模型更稳缺点是丢掉幅度信息涨5%的股票和涨0.1%的股票在标签里完全一样。排序学习。直接优化配对比较或排序指标比如LambdaMART。在选股场景里很自然模型不关心绝对收益值只关心排序这和最终TopN组合的逻辑高度一致。本项目主模型是LightGBM回归同时用XGBoost、随机森林做对照外加一个分类模型做信号辅助。3.2 模型评估IC、RankIC和分层回测单看准确率在选股问题里没有意义。我一般看三个指标IC截面相关系数每个交易日计算所有股票预测值和实际收益的Pearson相关系数再取时间均值。RankIC把上面的Pearson换成Spearman相关系数更关注排序关系。ICIR平均RankIC除以RankIC标准差衡量预测稳定性。下面是我项目里某一版模型的对照数据让大家对量级有个感觉。模型平均RankICRankIC胜率年化波动率LightGBM回归0.05263.1%18.5%XGBoost回归0.04861.7%19.2%随机森林回归0.01954.3%21.4%线性回归0.01152.6%19.9%注意财务数据的IC能到0.05已经算不错了别拿CTR预估那种0.2以上的IC来比完全不是一个数量级的问题。很多人拿到源码后看到RankIC只有0.05觉得模型“太弱”其实财务预测本身就很难0.05已经足够做出有区分度的选股组合了。3.3 时间序列交叉验证与防过拟合设置简单K-Fold不能用原因还是数据泄漏。如果第N天和第N1天的数据分别落在训练集和验证集它们的高度相关性会让验证集表现看起来漂亮得不真实。我用的方案是先按时间顺序切块再在训练块上做滚动前推验证也就是walk-forward validation。import lightgbm as lgb from sklearn.model_selection import TimeSeriesSplit params { objective: regression, metric: rmse, learning_rate: 0.02, num_leaves: 45, feature_fraction: 0.7, lambda_l2: 8, verbose: -1 } tscv TimeSeriesSplit(n_splits5) for train_idx, val_idx in tscv.split(X): train_data lgb.Dataset(X.iloc[train_idx], labely.iloc[train_idx]) val_data lgb.Dataset(X.iloc[val_idx], labely.iloc[val_idx]) model lgb.train( params, train_data, num_boost_round3000, valid_sets[val_data], callbacks[lgb.early_stopping(300)] )LightGBM拟合财务数据非常容易过拟合我调参的经验是learning_rate设小一点0.02左右num_leaves控制在30到60之间feature_fraction设0.7lambda_l2给到5到10。别一上来就开大轮数配合早停观察验证集上的RankIC不再上升就停。参数我常用的值说明learning_rate0.02财务数据信噪比低小学习率更稳num_leaves45叶子数太多容易过拟合feature_fraction0.7每次迭代随机抽70%特征lambda_l28L2正则抑制噪声特征min_data_in_leaf100叶子节点最少样本数避免学出极端规则3.4 多模型融合比单模型调参更划算做了几十组实验之后我发现把LightGBM和XGBoost的预测分数等权平均再和分类模型的概率分数做一次简单加权RankIC比任何一个单模型都稳定而且能降低某些极端月份的方差。原因是不同模型学到的偏误不一样集成可以有效对冲掉一部分“模型偏好”这比在单一模型上调来调去性价比高很多。这个做法简单说就是别迷信某个模型的精确度要相信多个模型的共识。4. 从模型分数到投资组合不只是取前N只股票模型输出的是一串预测分数不是仓位。怎么把分数变成组合是决定最终收益曲线形态的关键环节很多人在这里想得太简单。4.1 分数转权重的三种做法TopN等权。最简单每个月取分数最高的N只股票等权买入。适合验证模型信号但不适合真金白银因为它的换手率高、行业集中度不可控。分数加权。权重等于分数与阈值的差值占比超过阈值才持有。优势是保留模型对弹性的判断比如某期top股票分数明显更高组合会给它更大权重。组合优化。在给定风险预算、行业限制、个股权重上限下让组合的风险收益比最优。这也是源码默认推荐的方向。4.2 组合约束的设定如果直接把分数最高的一批股票塞进组合你会发现行业集中度特别高某个时间段可能全是新能源另一个时间段全是银行。行业中性化在组合层还需要再做一次让组合的行业暴露尽量贴近基准。我常用的约束组合是单一行业暴露相对基准不超过正负5%单只个股权重不超过5%持仓数量20到30只调仓频率20个交易日一次。20日调仓比较平衡太高换手会让手续费吃掉alpha太低又跟不住信号衰减。4.3 组合优化的数学表达与实操细节组合优化我用的是cvxpy目标函数是最大化预测分数带来的预期收益减去风险惩罚项风险用历史协方差矩阵估计。import cvxpy as cp import numpy as np # score: 模型输出的预测分数向量 # cov: 历史收益率协方差矩阵 # lam: 风险厌恶系数 n len(score) w cp.Variable(n) expected_term score w risk_term cp.quad_form(w, cov) objective cp.Maximize(expected_term - lam * risk_term) constraints [ cp.sum(w) 1, w 0, w 0.05, industry_exposure w max_industry, industry_exposure w -max_industry ] prob cp.Problem(objective, constraints) prob.solve() weights w.value这里有一个细节值得提醒协方差矩阵直接在全部股票上估计结果经常是病态的尤其当股票数量超过样本期长度的时候。务实做法是估计样本只取组合池中的候选股票或者用收缩估计器压缩极端相关系数。cvxpy只是求最优解的工具问题定义才是真正决定结果质量的部分。如果你在实盘里跑组合优化后经常得到极端权重优先检查协方差矩阵是不是出了问题而不是检查求解器设置。5. 回测结果可信吗五个隐藏的作弊点回测是量化研究里最容易被骗的环节。我见过太多口头说“年化50%”的策略一到实盘就垮掉问题基本都出在下面五个地方。5.1 幸存者偏差用今天的股票名单回测昨天的市场最经典的坑。如果你用2024年的股票名单去回测2018年的策略那所有2018年以后退市的公司都被排除了回测结果自然光鲜。数据集的构建必须使用当时存在的股票池。我在项目数据里保留了退市标记和上市日期就是希望使用者能注意到这件事。做基本面回测时股票池应该每年调整一次把当年已经退市或者之后退市的股票保留在回测区间内这样才接近真实选择环境。5.2 未来函数三个隐蔽的泄漏入口前面讲过财报对齐和截面标准化这里再点名一个隐蔽的标签泄漏。比如把未来20日收益生成训练标签以后回测时把同一个字段直接拿来做信号那就等于把答案提前拿出来了。训练特征和回测信号的构造必须完全独立。遇到这种情况唯一可靠的排查方法是写单元测试分别检查特征生成和标签生成两个模块的输出时间戳确保没有跨越公告日期的数据流动。5.3 交易成本与涨跌停约束回测里最常见的隐性偷懒是手续费和印花税设太低。A股双边成本大概在千二到千三如果调仓频繁这个成本非常致命。另外涨跌停板买不进、卖不出也要考虑尤其是小市值股票模型一给出信号市场早就涨停了你根本买不上。好的回测引擎至少要模拟这几类限制手续费、印花税、滑点、停牌、涨跌停无法成交。哪怕是粗略模拟也比完全不考虑要好得多。5.4 反复调参导致的验证集污染同一个验证集你来回调了20次参数理论上它已经不再是验证集而是训练集的一部分。模型在验证集上的表现会稳步提升但到真正没见过的数据上立刻现原形。标准解法是滚动前推验证每次只回测一小段未来数据调参后从不重叠的新数据上重新验证。这个方式和实盘的迭代节奏更接近虽然运行时间长一点但得到的结果更可信。5.5 样本外衰减机器学习模型实盘性能打几折不管是多好的模型样本外性能都会打折关键看特征本身稳不稳定。财务类特征变化慢衰减相对温和市场微观结构类特征衰减极快。应对策略有几个定期重训练比如每月或每季度重训一次多模型集成降低单一模型失效的风险在组合层做风险因子约束防止模型悄悄改变风格暴露。我自己的经验是一个RankIC在回测里0.05的模型实盘前三个月能保住0.03以上就已经算成功后续要看重训练节奏能不能跟上市场变化。6. 项目源码与数据集拿到手之后该怎么跑起来最后聊聊这套项目本身。源码和数据集已经整理成可直接运行的结构README里有完整的运行说明这里我讲一下最容易困惑的部分。6.1 目录结构与数据集字段说明源码包大致结构如下fundamental_ml_quant/ ├── data/ │ ├── raw/ # 原始财报与行情数据脱敏后 │ └── processed/ # 特征表与标签 ├── features/ │ ├── alignment.py # 公告日期对齐 │ └── construction.py # 特征构造与标准化 ├── models/ │ ├── train.py # 模型训练与评估 │ └── ensemble.py # 多模型融合 ├── portfolio/ │ ├── optimize.py # 组合优化 │ └── constraints.py # 约束定义 ├── backtest/ │ └── engine.py # 回测引擎 └── README.md数据集的字段包括股票代码、交易日期、公告日期、报告期、主要财务指标、日线行情、行业分类、退市标记。注意这是脱敏后的数据时间覆盖和你自己拿到的实盘数据可能对不齐但字段含义和逻辑完全一致。6.2 运行环境与依赖Python 3.9以上依赖pandas、numpy、scikit-learn、lightgbm、xgboost、cvxpy、matplotlib。建议直接用conda创建虚拟环境避免依赖冲突。6.3 三步跑通示例pip install -r requirements.txt python features/construction.py # 生成特征表 python models/train.py # 训练并评估模型 python backtest/engine.py # 跑回测并输出净值曲线正常来说跑完三步会输出一份业绩报告包含累计净值、年化收益、最大回撤、夏普比率和换手率统计。如果你看到某个步骤执行时间特别长优先检查是不是数据对齐那一步用了双重循环可以改成merge_asof性能会提升不少。6.4 源码研究的学习建议我不建议一上来就改模型参数。先看features/alignment.py理解公告日期对齐为什么最关键再看models/train.py里的评估函数把IC、RankIC的算法过一遍最后才动组合优化。等你能完整解释每一行代码背后为什么这么写的时候这个项目才算真正吃透了。说实话把这套系统完整跑下来之后我最大的体会不是机器学习多厉害而是数据管道只要差一点点模型就会差得离谱。我个人建议如果你想做类似的研究可以先不做那么复杂的组合优化哪怕只用TopN等权加LightGBM回归只要把未来函数彻底防住结果就已经能说明很多问题了。后来我在源码里写注释最多的地方恰恰是数据对齐和去极值那几行而不是模型本身。最后再提一个小技巧保存模型和标签的时候把版本号写清楚不然三个月后你根本不知道自己回测的是哪个版本的数据这个坑我踩过不止一次。本文还有配套的精品资源点击获取
返回列表