1. 项目概述:一个基于机器学习的股票预测入门项目
如果你对量化交易和机器学习感兴趣,并且想找一个能让你亲手实践、理解整个流程的起点项目,那么MachineLearningStocks这个项目绝对值得你花时间研究。它最初由一位开发者作为自己的第一个Python项目创建,后来经过多次迭代,成为了一个结构清晰、高度可扩展的模板。这个项目的核心目标很直接:利用历史基本面数据(如市盈率、负债权益比等)和机器学习模型,来预测哪些股票在未来一年内会跑赢大盘指数(如标普500)。
简单来说,它试图回答一个每个投资者都关心的问题:“根据一家公司当前的财务状况,我们能判断出它明年会是赢家吗?”这个项目没有承诺一夜暴富的“圣杯”,而是提供了一个完整的、可复现的框架,让你能理解从数据获取、清洗、建模到回测的每一个环节。原作者曾基于此框架的变体进行过实盘交易,取得了还算不错的结果(回测约20%,实盘约10-15%的年化收益),但他也明确强调,这纯粹是教育性质的项目,回测表现可能具有欺骗性,实盘交易风险自担。
对于初学者,这是一个绝佳的“脚手架”。它能帮你避开从零搭建时遇到的那些令人头疼的细节陷阱,比如如何从混乱的HTML中解析财务数据,如何处理缺失的交易日数据,以及如何构建一个合理的标签(即“跑赢大盘”的定义)。对于有一定经验的从业者,这个项目的代码结构和设计思路也提供了很好的参考,尤其是其强调的可扩展性,你可以轻松地替换数据源、尝试不同的特征工程方法、或者接入更复杂的模型。
接下来,我将为你深入拆解这个项目的每一个核心环节,不仅告诉你代码是怎么运行的,更会解释为什么要这么做,以及在实践过程中可能遇到的“坑”和应对技巧。
2. 核心思路与工作流拆解
在动手写代码之前,我们必须先理清整个项目的逻辑脉络。一个基于基本面的机器学习股票预测系统,其工作流可以抽象为以下七个步骤,这也是本项目代码组织的骨架:
2.1 端到端的工作流
- 获取历史基本面数据:这是我们的特征(Features/Predictors)。我们需要知道公司在历史某个时间点(例如,2005年1月28日)的财务状况,比如它的市盈率、利润率、负债情况等。本项目使用了一个从Yahoo Finance解析的免费数据集(2003-2013年标普500成分股数据)。
- 获取历史股价数据:这是我们的标签(Label/Target)。我们需要知道在上述基本面“快照”之后的一段时间里(例如,一年后),该股票的实际表现如何。这里我们同时需要个股和标普500指数的价格数据,因为我们的目标是预测“相对收益”。
- 数据预处理与特征工程:原始数据通常是“脏”的。我们需要清洗(处理缺失值、异常值)、对齐(确保基本面日期和股价日期对应),并可能从原始特征中构造出更有预测力的新特征。
- 训练机器学习模型:使用清洗后的历史数据,训练一个分类器(本项目默认使用
scikit-learn的RandomForestClassifier)来学习基本面特征与未来是否跑赢大盘之间的关系。 - 模型回测:在历史数据上模拟交易策略,以评估模型的有效性。这是检验策略是否只是“过拟合”历史数据的关键一步。请注意,本项目提供的回测脚本存在一个常见但严重的缺陷(前视偏差),会导致结果过于乐观,后文会详细分析。
- 获取当前基本面数据:模型训练好后,我们需要获取当前最新的公司基本面数据,作为模型进行预测的输入。
- 生成预测信号:将当前数据输入训练好的模型,得到预测结果,即判断哪些股票当前值得买入(预期将跑赢大盘)。
这个流程形成了一个从数据到决策的闭环。本项目的每个核心脚本都对应着其中一个或几个环节。
2.2 为什么选择基本面数据与相对收益?
这是一个关键的设计选择。市场上有很多策略基于技术指标(如移动平均线、RSI),那为什么这里要用基本面?
- 逻辑可解释性:基本面数据反映了公司的内在健康状况。投资于财务稳健、增长潜力大的公司,是价值投资的核心理念。机器学习在这里的作用是量化这种关系,并找出人类可能忽略的复杂模式。
- 中长期视角:基本面变化通常较慢,基于它的预测更适合中长期投资(如本项目采用的一年期),而非高频交易。这降低了交易频率和摩擦成本。
- 相对收益作为目标:直接预测股价的绝对涨跌非常困难,因为它受整个市场牛熊市的系统性影响极大。将目标定义为“是否跑赢标普500指数”,相当于让模型专注于寻找相对于市场的超额收益(Alpha),这剥离了市场整体波动的影响,是一个更聪明、也更实际的目标设定。
实操心得:在开始任何量化项目前,花时间清晰地定义你的预测目标和信号生成频率至关重要。是预测明天涨跌、下周收益率,还是一年后的相对表现?不同的目标决定了你需要什么样的数据、什么样的模型,以及如何评估结果。本项目的一年期相对收益预测,决定了它需要年度基本面数据和年度价格回报率来计算标签。
3. 数据获取与处理的魔鬼细节
数据环节是量化策略的基石,也是最繁琐的部分。本项目的原始数据主要来自Yahoo Finance,但获取和处理过程充满挑战。
3.1 历史基本面数据的获取与解析
项目最初依赖pandas-datareader从Yahoo Finance直接获取基本面和价格数据。但由于Yahoo频繁更改其API和网页结构,直接获取变得不稳定。因此,项目转而使用一个预先处理好的离线数据包(intraQuarter.zip),其中包含了从Yahoo Finance HTML页面抓取并保存的原始数据。
核心挑战:从HTML中解析数据文件parsing_keystats.py承担了最“脏”的活。它需要遍历成千上万个HTML文件,从中提取出几十个财务指标。这里没有使用常见的HTML解析库如BeautifulSoup,而是采用了正则表达式(Regex)。
# 示例:用于提取特定指标的正则表达式模式 r'>' + re.escape(variable) + r'.*?(\-?\d+\.*\d*K?M?B?|N/A[\\n|\s]*|>0|NaN)%?(</td>|</span>)'这个表达式虽然看起来复杂,但逻辑是:
- 定位到特征名(如
Market Cap)。 - 跳过中间任意字符(
.*?),直到找到一个数字格式的值。 - 这个值可能是负数(
-)、包含千/百万/十亿缩写(K, M, B)、是N/A或NaN(缺失值),或者是一个百分比。 - 值后面紧跟着HTML标签的结束(
</td>或</span>),表明这是该表格单元格的结尾。
重要提示:在专业环境中,通常不推荐使用正则表达式解析HTML,因为HTML结构复杂且易变,正则表达式难以处理嵌套标签等复杂情况,容易出错且难以维护。BeautifulSoup或lxml是更健壮的选择。本项目使用正则表达式,主要是出于教育目的,让代码逻辑更直观。在实际项目中,如果Yahoo页面结构再次变化,这个解析器很可能失效,你需要用更稳定的解析工具重写这部分代码。
运行python parsing_keystats.py后,你会得到keystats.csv文件。这个文件每一行代表一只股票在某个特定日期的基本面“快照”,包含了数十个特征。
3.2 历史价格数据的对齐与标签计算
获取股价数据(download_historical_prices.py)相对直接,可以使用pandas-datareader或Quandl。但这里有一个关键问题:日期对齐。
我们的标签是“从基本面报告日期起,未来一年的股票回报率减去同期指数回报率”。如果基本面报告日期是2005年1月28日(周五),我们需要计算到2006年1月28日的价格变化。但2006年1月28日可能是周末(市场休市),没有价格数据。
解决方案:前向填充(Forward Fill)项目的处理方式是,如果目标日期没有交易数据,则使用该日期之前最近的一个交易日的数据。这基于一个合理的假设:周末或假期的股价与节前最后一个交易日收盘价相同。在pandas中,这可以通过asfreq(‘B’).ffill()等方法轻松实现。
标签计算步骤:
- 对于
keystats.csv中的每一行(股票A,日期T),找到股票A在日期T和日期T+365天(或前向填充后的最近交易日)的调整后收盘价。 - 计算股票在期间的简单回报率:
(Price_{T+365} - Price_T) / Price_T。 - 找到标普500指数在相同两个日期的价格,计算指数回报率。
- 计算超额回报:
股票回报率 - 指数回报率。 - 根据设定的
OUTPERFORMANCE参数(默认为10%),将连续的超额回报转化为二分类标签:如果超额回报 >OUTPERFORMANCE,则标记为1(买入/跑赢),否则为0(不买入/未跑赢)。
注意事项:
- 价格选择:务必使用调整后收盘价,它考虑了分红、拆股等公司行为,能真实反映投资回报。
- 数据一致性:确保股价数据和基本面数据的股票代码(Ticker)完全匹配,并且时区一致(通常为美国东部时间)。
- 幸存者偏差:这个数据集只包含了至今仍存在的标普500成分股。那些在此期间退市或破产的公司被排除在外了,这会导致回测结果偏高,因为模型没有学习到“失败”的案例。在更严谨的策略中,需要使用点-in-time数据库,确保在任何决策时刻,你只知道当时存在的公司。
4. 机器学习模型构建与核心实现
数据准备好后,就进入了建模环节。本项目的机器学习部分相对标准,使用了scikit-learn库,但其背后的思考过程值得深究。
4.1 特征与模型选择
特征:就是keystats.csv中解析出的所有基本面指标,如市盈率、市净率、利润率、负债率等。这些特征量纲差异巨大(例如,市值是数十亿,而利润率是零点几),但本项目使用的随机森林模型对特征尺度不敏感,因此没有进行标准化处理。如果换用SVM或神经网络,这一步则是必须的。
模型:默认使用RandomForestClassifier(随机森林分类器)。这是一个集成学习模型,通过构建多棵决策树并综合它们的投票结果来做决策。
为什么选择随机森林?
- 非线性关系:基本面与股价之间的关系很可能是非线性的,决策树类模型能很好地捕捉这种复杂交互。
- 特征重要性:随机森林可以输出特征重要性评分,帮助我们理解哪些财务指标对预测贡献最大,这具有很好的业务解释性。
- 抗过拟合:通过“随机森林”的机制(行采样、列采样),它在一定程度上减少了单棵决策树容易过拟合的风险。
- 开箱即用:通常不需要复杂的调参就能获得不错的效果,适合作为基线模型。
4.2 训练流程与关键代码解析
核心训练逻辑包含在数据预处理和模型拟合中。虽然项目将回测和训练写在一起,但我们可以拆解其步骤:
- 数据加载与清洗:读取
keystats.csv,删除包含缺失值(NaN)的行。这是一个简单的处理方式,但会损失大量数据。更精细的做法可以考虑插值(如用行业均值填充)或使用能处理缺失值的模型(如XGBoost)。 - 特征与标签分离:将数据框分为特征矩阵
X和标签向量y。 - 训练集-测试集划分:这里有一个至关重要的细节。在时间序列数据中,我们不能使用随机划分。必须按时间顺序划分,例如用2003-2010年的数据训练,用2011-2013年的数据测试。这样可以模拟在历史数据上训练,在未来数据上预测的真实场景,避免“数据泄露”。原项目的
backtesting.py脚本需要仔细检查其数据划分方式是否符合此原则。 - 模型训练:使用
RandomForestClassifier的fit方法在训练集上进行训练。 - 模型评估:在测试集上计算准确率、精确率等指标。但请记住,在金融领域,这些统计指标远不如在回测中的资金曲线来得重要。
4.3 一个必须警惕的回测陷阱
原项目的backtesting.py脚本中,潜藏着一个在量化回测中非常经典且致命的错误:前视偏差(Look-ahead Bias)。
简单来说,前视偏差是指在构建策略时,使用了在当时那个时间点还无法获得的信息。在本项目的语境下,可能的表现形式有:
- 信息泄露:在构建特征或标签时,不小心使用了未来的数据。例如,如果用到了需要财报发布日后才能计算出的指标,但在模型中使用的是财报发布日期当天的“快照”,这就造成了泄露。
- 不切实际的交易假设:回测中假设可以以当天收盘价买入,但实际上你的交易信号是基于收盘后才能获得的基本面数据生成的,你最早只能在下一个交易日开盘时买入。这会导致买入价估值不准确。
- 幸存者偏差:如前所述,只使用了最终存活下来的股票数据。
原作者在文档中暗示了回测脚本存在一个“微妙”的缺陷,并让读者自己去发现。这个缺陷很可能就是某种形式的前视偏差。例如,在计算“年度回报”时,是否严格确保了只使用过去的数据?在划分训练集和测试集时,是否保证了时间上的严格隔离?
如何修正?一个健壮的回测需要实现走前检验(Walk-Forward Analysis)。其过程如下:
- 确定一个初始训练窗口(如2003-2007年),用其训练模型。
- 在下一个时间单元(如2008年)上测试模型,生成交易信号并模拟交易,记录收益。
- 将测试期(2008年)的数据加入到训练数据中(或滚动窗口),重新训练模型。
- 在下一个时间单元(2009年)上测试,如此反复,直到遍历所有数据。 这样每一步的预测都严格基于历史信息,模拟了实盘交易中模型随时间更新的过程。
5. 从预测到实践:生成当前信号与策略改进
当模型通过回测验证(以正确的方式)后,我们就可以用它来处理当前数据了。
5.1 获取当前数据与生成预测
脚本current_data.py会模拟parsing_keystats.py的过程,但目标是抓取当前Yahoo Finance上各股票的最新基本面数据。运行后会生成forward_sample.csv,其格式与keystats.csv类似,但数据是最新的。
随后,stock_prediction.py脚本会加载训练好的模型和forward_sample.csv,对每只股票进行预测,并筛选出那些被模型标记为“1”(即预测其未来一年超额收益超过OUTPERFORMANCE阈值)的股票列表。
# 运行预测脚本 python stock_prediction.py # 输出示例 21 stocks predicted to outperform the S&P500 by more than 10%: NOC FL SWK NFX LH NSC SCHL KSU DDS GWW AIZ ORLY R SFLY SHW GME DLX DIS AMP BBBY APD这个列表就是你的“候选买入清单”。
5.2 超越模板:策略改进的无限可能
本项目是一个强大的起点,但绝非终点。以下是一些可以深入探索的改进方向,这也是量化研究真正开始的地方:
1. 数据层面的深化:
- 更多更好的数据:基本面数据只是冰山一角。可以引入:
- 另类数据:社交媒体情绪、供应链数据、卫星图像(如停车场车辆数)、招聘信息等。
- 高频数据:虽然本项目是中长期策略,但日内订单流数据可以辅助判断市场微观结构。
- 全球市场数据:在流动性较差的新兴市场,错误定价的机会可能更多。
- 更精细的特征工程:
- 衍生特征:不要只使用原始比率。计算一些价值投资经典指标,如格雷厄姆数、Altman Z-Score(破产风险评分)等。
- 行业相对值:将公司的PE与其所在行业的平均PE比较,而不是用绝对值。
- 时间序列特征:计算基本面指标的变化率、动量(如最近四个季度的利润增长趋势)。
- 处理缺失值:直接删除缺失行是下策。可以考虑多重插补、使用类似公司的均值填充,或者使用像LightGBM这类能原生处理缺失值的模型。
2. 模型与算法的进化:
- 尝试不同的模型:
- 梯度提升树:如XGBoost、LightGBM、CatBoost,它们在许多表格数据竞赛中表现卓越。
- 深度学习:使用多层感知机或更复杂的结构处理特征。可以尝试用
sklearn.neural_network入门。 - 集成模型:将随机森林、梯度提升树等模型的预测结果进行加权平均或堆叠。
- 改变预测任务:
- 回归问题:不预测“是否跑赢”,而是直接预测“超额收益的数值”。这能提供更多信息,但可能更难。
- 排序学习:预测股票的相对排名(哪只股票在组内表现最好),而不是绝对分类。
- 超参数优化:使用网格搜索、随机搜索或贝叶斯优化工具来为你的模型和数据集找到最优的超参数组合。
3. 投资组合构建:这是本项目未涉及但至关重要的最后一步。模型告诉你买哪些股票,但没告诉你每只股票买多少。
- 等权配置:最简单的办法,每只股票投入相同金额。
- 基于预测置信度加权:对模型预测概率高的股票分配更高权重。
- 现代投资组合理论:这正是原作者另一个项目
PyPortfolioOpt的用途。它可以根据股票的预期收益、风险(波动率)以及股票间的相关性,计算出在给定风险水平下收益最大化的投资组合权重,或给定收益下风险最小化的权重。将ML的预测结果作为预期收益输入给MPT,是一个很自然的结合。
4. 执行与风控:
- 交易成本:回测中必须考虑佣金、印花税和滑点(尤其是对于流动性差的股票),否则回测收益会被严重高估。
- 头寸规模与风险管理:设定单只股票的最大仓位、整个组合的最大回撤止损线。
- 再平衡周期:是按年度跟随模型预测调仓,还是季度、月度?不同的周期对收益和交易成本影响很大。
6. 常见问题、排错与实操心得
在实际运行和扩展这个项目时,你几乎一定会遇到各种问题。以下是一些典型问题及解决思路:
Q1: 运行python download_historical_prices.py时失败或数据不全。
- 原因:Yahoo Finance的免费API不稳定,经常更改或限制访问。
- 解决方案:
- 使用替代数据源:这是最根本的解决办法。可以考虑:
- Quandl:有部分免费数据,API稳定。
- Alpha Vantage:提供免费的API,但有频率限制。
- IEX Cloud:提供免费层。
- 本地金融数据库:如
akshare(中文)、yfinance库(对Yahoo API的封装,有时更稳定)。
- 使用项目提供的备份数据:作者在仓库中上传了
stock_prices.csv和sp500_index.csv作为备用,你可以直接使用这些文件跳过下载步骤。
- 使用替代数据源:这是最根本的解决办法。可以考虑:
Q2: 运行python current_data.py时解析出错,forward_sample.csv为空或数据奇怪。
- 原因:Yahoo Finance的网页结构发生了变化,导致正则表达式无法正确匹配。
- 解决方案:
- 手动检查:打开一个股票的Statistics页面(如
https://finance.yahoo.com/quote/AAPL/key-statistics?p=AAPL),查看网页源代码,确认你要抓取的数据所在的HTML标签和结构是否还和正则表达式匹配。 - 改用更健壮的解析器:这是长期解决方案。使用
BeautifulSoup或lxml库来解析HTML。你可以根据新的页面结构重写数据提取逻辑。例如,用BeautifulSoup通过CSS选择器或标签属性来定位数据,远比正则表达式稳定。 - 寻找稳定的API:同上,考虑付费或更稳定的数据源。
- 手动检查:打开一个股票的Statistics页面(如
Q3: 模型准确率很高(比如80%),但回测收益却不理想,甚至亏损。
- 原因:
- 前视偏差:这是最可能的原因。请严格检查你的回测逻辑,确保没有任何信息泄露。
- 样本内过拟合:模型在训练集上表现太好,但在未知的测试集(或未来的数据)上泛化能力差。可能因为模型太复杂、特征噪音大。
- 交易成本与滑点:高换手率的策略会被交易成本侵蚀利润。
- 市场状态变化:模型在过去十年有效的规律,在当前市场环境下可能已经失效(即“因子衰减”)。
- 解决方案:
- 实施走前检验:这是检验策略稳健性的黄金标准。
- 简化模型:减少特征数量,使用正则化,或选择更简单的模型。
- 在回测中加入成本模型。
- 持续监控与迭代:量化策略不是一劳永逸的,需要定期根据新数据重新评估和调整。
Q4: 预测出的股票列表每次运行都不一样(对于随机森林)。
- 原因:随机森林具有随机性(随机选择样本和特征建树)。即使设置了随机种子,如果数据或参数有细微变动,结果也可能不同。
- 解决方案:
- 设置随机种子:在代码中
import random; import numpy as np; import sklearn后,分别设置它们的随机种子,以确保结果可复现。 - 多次运行取共识:运行预测多次,选择那些最常出现在“买入列表”中的股票。这可以增加信号的稳定性。
- 使用确定性更强的模型:如逻辑回归或支持向量机(需调整好参数)。
- 设置随机种子:在代码中
实操心得与最后建议:
- 从复现开始:不要一开始就想着改进。先严格按照README,让项目在你的本地环境成功跑通,理解每一行代码的作用。
- 拥抱错误:数据获取和解析的错误是常态。学会阅读错误信息,使用打印中间变量、断点调试等方式定位问题,这是量化工程师的日常。
- 怀疑一切,尤其是亮眼的回测结果:如果某个策略看起来好得不像真的,那它很可能就是假的。深入检查每个环节,特别是数据清洗、标签计算和回测逻辑。
- 从小处着手改进:不要试图一次性改造所有部分。比如,先尝试用BeautifulSoup重写解析器;成功后再尝试加入一两个新特征;然后再尝试换用XGBoost模型。每次只改变一个变量,你才能知道是什么带来了提升(或下降)。
- 重视代码版本管理:使用Git。每次对策略进行重大修改前,创建一个新的分支。这样如果新想法失败了,你可以轻松回退到稳定的版本。
这个项目就像一副骨架,它为你勾勒出了“基本面量化”的完整形态。而真正的血肉——更优质的数据、更精巧的特征、更稳健的模型、更严谨的风控——则需要你用自己的知识和创造力去填充。记住,在量化交易的世界里,对细节的偏执和对逻辑的严谨,是区分业余爱好者与专业从业者的关键。