☰
机器学习预测股票月度收益:逻辑回归、梯度提升与LSTM多模型策略解析
2026/10/3 2:46:28 网站建设 项目流程

简介:一份基于逻辑回归、梯度提升与LSTM三种模型的股票收益预测项目,面向金融量化爱好者、计算机相关专业学生及从业者。项目以基本面和情绪特征为输入,预测下个月股票收益的正负,通过每月筛选预测收益为正的前20%股票进行多头投资,并对市场做空以构建市场中性组合,策略思路清晰且贴近实践。资源包含可运行的Python源码、Jupyter Notebook分析说明、答辩PPT以及16张策略表现可视化图表,共20个文件,覆盖数据探索、模型构建、回测评估到展示汇报全流程;压缩包仅1.8MB,轻量易用,目录结构便于按需查看。源码经测试通过,适合课程设计、毕业设计或量化入门实战参考,作者支持远程教学。目前已有115人学习,配合英文说明文档可快速理解三种模型的预测逻辑、样本外表现及多空组合构建细节,适合作为从数据到策略落地的完整参考。

1. 三个模型预测下月股票收益:这份源码到底能帮你搞定什么

做量化的人都知道,单模型预测股票收益的正负,翻车概率极高——市场噪音太多,逻辑回归容易欠拟合,LSTM又经常过拟合到看不出规律。这份资源把逻辑回归、梯度提升和LSTM放在同一个框架里跑月度收益预测,每月选出预测收益为正的前20%股票,对其中一半做多头,同时对市场指数做空头,构建一个市场中性组合。它解决的核心问题不是“哪只股票会涨”,而是“在控制市场风险的前提下,下个月哪些股票有更高概率跑赢”。适合正在做课程设计、毕业设计,或者刚接触基本面量化、想找一个完整可跑的基准策略的人。源码里附带策略说明文档、Pitchbook和整套回测图表,下载后直接对着 Notebook 复现整个流程,比自己从零搭要快得多。

2. 数据准备与特征工程:Quandl 数据源和月度标签怎么对齐

2.1 数据源选型:为什么用 Quandl 而不是 Tushare 或 yfinance

这份源码的数据层用的是 Quandl 数据库。Quandl 现在已经整合进 Nasdaq Data Link,但它的历史接口和表格结构在学术界和量化比赛里用得非常多,尤其是 Sharadar 基本面表和价格表。选择 Quandl 而不是 Tushare 或 yfinance,原因有三个:第一,Quandl 的表格接口对基本面数据(利润表、资产负债表、现金流)和价格数据做了标准化对齐,字段命名统一,不需要自己清洗不同来源的字段名;第二,它通过 API key 就能拉取,适合在 Notebook 环境里快速做原型验证;第三,很多经典量化论文的数据集都基于 Quandl,复现时对照文献更方便。

不过要注意,Quandl 免费层有调用次数限制,而且部分表格(比如 Sharadar 的 SF1 表)需要订阅。如果你在运行源码时发现数据拉不下来,常见做法是换成 Tiingo 或者直接读取本地 CSV。但换数据源后,字段名映射要做一遍,尤其是日期格式、ticker 格式和基本面指标的单位,否则后面特征拼接会出问题。

import quandl import pandas as pd quandl.ApiConfig.api_key = "YOUR_API_KEY" # 拉取价格数据 prices = quandl.get_table("SHARADAR/SEP", ticker="AAPL", date={"gte": "2015-01-01", "lte": "2023-12-31"}) # 拉取基本面数据 fundamentals = quandl.get_table("SHARADAR/SF1", ticker="AAPL", dimension="ARQ", date={"gte": "2015-01-01", "lte": "2023-12-31"})

这段代码先取价格表,再取季度基本面表。dimension="ARQ"表示按季度报告的 as-reported 数据,用这个而不是MRQ(最最近一个季度)是因为回测时要用当时实际能拿到的数据,避免前视偏差。日期范围建议稍微扩大一点,给特征构造留出滞后窗口。

2.2 标签构造:下个月收益为什么用“符号”而不是“幅度”

源码里模型的预测目标是下个月收益的正负,而不是具体的涨跌幅。这是二分类问题的标准做法,背后逻辑很直接:预测方向比预测幅度更稳定,对噪音的容忍度更高。幅度预测的误差在回归任务里会被平方放大,而符号预测即使幅度错了,只要方向对,策略依然能赚钱。

标签的具体构造方法是:假设今天为 t 日,取 t+1 月到 t+2 月的收益率作为标签。更精确地说,用月末收盘价计算下个月的收益,然后取符号,正为 1,负为 0。源码里应该是用了pct_change加shift的组合来实现这个对齐。

# 月度重采样,取每月最后一个交易日收盘价 monthly = prices["close"].resample("M").last() # 下月收益 = 下月收盘 / 本月收盘 - 1 monthly_ret = monthly.pct_change().shift(-1) # 标签:下月收益 > 0 记为 1,否则为 0 label = (monthly_ret > 0).astype(int)

逻辑说明:shift(-1)非常关键,它把收益往前移了一个月,让当前月的特征去预测未来的收益。如果不做这个 shift,模型就是在用本月的数据预测本月的收益,等于作弊。参数说明是resample("M")用的是月末最后一个交易日,如果数据里有停牌或退市,需要先用dropna清洗。

2.3 特征池:基本面因子和情绪因子怎么组合

这份策略强调同时使用基本面和情绪特征。基本面部分,典型的因子包括市盈率、市净率、企业价值倍数、毛利率变化、营收增速、资产负债率等。情绪部分,常见做法是用新闻情感打分、分析师评级变化或者搜索热度指数。源码里没有明确列出完整因子清单,但从图表命名来看,使用了 beta 控制和是否包含情绪特征的对比实验。

我一般会把特征分成三组:估值组、成长组、质量组,每组选 3 到 5 个因子。估值组用 PE、PB、EV/EBITDA;成长组用营收同比增速、利润同比增速;质量组用 ROE、毛利率、资产负债率。情绪特征如果拿不到新闻数据,可以先用动量因子(过去 6 个月收益)和波动率因子替代,效果接近。

features = pd.DataFrame({ "pe": fundamentals["pe"], "pb": fundamentals["pb"], "roe": fundamentals["roe"], "gross_margin": fundamentals["grossmargin"], "asset_turnover": fundamentals["assetturnover"], "mom_6m": prices["close"].pct_change(126), # 6个月动量 "vol_20d": prices["close"].pct_change().rolling(20).std() # 20日波动 })

参数说明:mom_6m用 126 个交易日近似 6 个月,vol_20d用 20 日滚动标准差。这些因子在进入模型前要做标准化,否则逻辑回归和 LSTM 的收敛速度会受影响。特别是 LSTM,输入尺度差异大时训练很容易发散。

3. 三种模型的核心差异与复现:逻辑回归、梯度提升、LSTM

3.1 逻辑回归:线性基准的意义和正则化参数选择

逻辑回归在这里的角色是“线性基准”。它的优势是解释性强、训练快、不容易过拟合,缺点是无法捕捉特征之间的非线性交互。源码把它作为第一个模型跑,本质上是为了给梯度提升和 LSTM 一个对比锚点——如果非线性模型跑不过逻辑回归,那说明特征工程本身有问题,而不是模型的问题。

逻辑回归的关键参数是正则化强度 C 和惩罚项类型。默认用 L2 正则化,C 值越小正则化越强。在月度截面数据上,特征维度通常不超过 20 个,样本量可能只有几百到几千,所以 C 值设在 0.1 到 1 之间比较稳妥。

from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_scaled = scaler.fit_transform(X_train) lr = LogisticRegression(C=0.5, penalty="l2", solver="liblinear", max_iter=1000) lr.fit(X_scaled, y_train) print("Train acc:", lr.score(X_scaled, y_train))

参数说明:solver="liblinear"适合小数据集和二分类,max_iter=1000是为了确保收敛。如果数据量超过几万条,可以换成solver="lbfgs",速度更快。C=0.5是我常用的起点,如果你发现训练集准确率很高但验证集崩了,就把 C 调小;反过来如果两个都低,先检查特征标准化和标签对齐,不要急着调参。

3.2 梯度提升:XGBoost 还是 LightGBM,参数怎么设

梯度提升模型在源码里对应的是 Gradient Boosting,实际实现大概率是 XGBoost 或 LightGBM 之一。这类模型的核心机制是串行训练多棵决策树,每棵树拟合前一棵树的残差。它对特征尺度不敏感,不需要标准化,而且能自动处理特征交互,这是它强过逻辑回归的地方。

金融时间序列上,我一般优先用 LightGBM,因为它的直方图算法在月度截面数据上训练速度更快。如果你是第一次跑这份源码,建议保持默认参数先跑通,再调n_estimators和learning_rate。

import lightgbm as lgb model_lgb = lgb.LGBMClassifier( n_estimators=300, learning_rate=0.05, max_depth=4, num_leaves=15, subsample=0.8, colsample_bytree=0.8, random_state=42 ) model_lgb.fit(X_train, y_train, eval_set=[(X_val, y_val)], eval_metric="auc", early_stopping_rounds=50)

参数说明:max_depth=4和num_leaves=15是控制树复杂度的关键,金融数据信噪比低,树太深容易学到噪音。subsample和colsample_bytree都是 0.8,做行采样和列采样防止过拟合。early_stopping_rounds=50表示验证集 AUC 连续 50 轮不提升就停止训练,这是防止过拟合最有效的手段。注意,eval_set必须用时间上靠后的数据,不能随机切分,否则会引入前视偏差。

3.3 LSTM:序列长度、隐藏层维度和 dropout 的取舍

LSTM 在这份源码里负责捕捉时间序列上的非线性依赖。和逻辑回归、梯度提升不同,LSTM 的输入是三维的:样本数 × 时间步长 × 特征维度。也就是说,它不是用当前月的特征做预测,而是用过去 N 个月的特征序列来预测下个月的收益方向。时间步长的选择是第一个要确定的超参数。

金融月度数据上,时间步长取 6 到 12 比较常见。太短(比如 3)模型学不到中期趋势,太长(比如 24)样本量不够,月度数据的有效样本本来就不多。隐藏层维度我一般设 32 到 64 之间,数据集小就取小值。

import torch import torch.nn as nn class LSTMClassifier(nn.Module): def __init__(self, input_size, hidden_size=32, num_layers=1, dropout=0.3): super().__init__() self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, dropout=dropout) self.fc = nn.Linear(hidden_size, 1) self.sigmoid = nn.Sigmoid() def forward(self, x): out, _ = self.lstm(x) out = self.fc(out[:, -1, :]) return self.sigmoid(out).squeeze()

这段代码用了 PyTorch 的nn.LSTM,batch_first=True让输入维度变成 (batch, seq_len, input_size)。out[:, -1, :]取最后一个时间步的隐藏状态作为全连接层的输入,这是序列分类任务的常见做法。dropout=0.3设置在 LSTM 层之间,如果只设一层 LSTM 这个参数不生效,需要num_layers=2才有效。训练时建议用nn.BCELoss作为损失函数,优化器用 Adam,学习率从 0.001 起。

LSTM 训练要特别小心:月度数据样本量通常只有几百到一千多,非常容易过拟合。我见过很多人把 LSTM 训到训练集准确率 98%,验证集 51%,这就是典型的数据量不足以支撑复杂模型的表现。缓解办法有三个——加大 dropout、减少时间步长、早停。

4. 策略落地:月度选股、多空组合与 beta 控制

4.1 筛选逻辑:前 20% 的股票为什么只做多一半

源码策略的逻辑是:每个月用训练好的模型预测全市场股票的收益方向概率,选出预测为正且概率最高的前 20% 股票,然后从中随机取一半做多,同时对市场指数做等金额的空头。这样做有两个目的:前 20% 的筛选保证了组合集中在模型最看好的股票上;只做多一半则是为了分散个股风险,避免某一只股票的黑天鹅把整个组合的 alpha 吃掉。

为什么不是对全部 20% 做多?因为月度调仓下,模型预测的准确率通常在 52% 到 58% 之间,即使是最看好的前 20%,也有接近一半会预测错。集中持仓会放大单票波动,分散到一半股票可以平滑收益曲线。这种做法在学术上叫 equal-weight portfolio,源码里的PortfolioPerformanceBetaCap_1_DataFrame图表对应的就是不同 beta 上限下的组合表现。

# 假设 pred_prob 是模型输出的下月上涨概率 top20 = pred_prob.nlargest(int(len(pred_prob) * 0.2)) # 随机选一半做多 long_pick = top20.sample(frac=0.5, random_state=42) # 等权配置 weights = pd.Series(1 / len(long_pick), index=long_pick.index) # 市场做空:市值等额 market_notional = portfolio_value short_weights = -weights * (long_pick["mktcap"] / market_notional)

参数说明:nlargest是 Pandas 里取前 N 大值的方法,比sort_values + head更简洁。sample(frac=0.5)表示随机抽一半,random_state固定下来是为了回测可复现。这里权重用等权,不按市值加权,因为月度截面下市值加权会让组合偏向大盘股,失去小盘股的高 alpha 来源。

4.2 市场中性:空头头寸怎么对冲系统性风险

市场中性策略的核心是组合的 beta 接近 0,这样无论大盘涨跌,组合收益主要来自选股 alpha。源码里用“对市场做相应的空头头寸”来实现,具体做法是对做多股票的多头市值,融券卖出等额的市场指数——通常是标普 500 或沪深 300。但实际操作中,个股的 beta 不是 1,所以需要对冲比例做缩放。

这就是图表里PortfolioPerformanceBetaCap_1这个名字的含义——把组合的 beta 上限限制在 1。如果做多组合的 beta 是 1.3,那就应该空头 1.3 倍市值的指数,整体 beta 归零。不过月度调仓时 beta 会漂移,所以源码里应该是做了 beta 上限约束,而不是精确对冲。

# 计算组合加权 beta portfolio_beta = (weights * stock_beta).sum() # 目标 beta 为 0,空头规模 = 多头市值 * portfolio_beta hedge_notional = long_notional * portfolio_beta # 如果组合 beta 超过上限 1,强制降低多头仓位 beta_cap = 1.0 if portfolio_beta > beta_cap: scaled_weights = weights * (beta_cap / portfolio_beta) hedge_notional = long_notional * beta_cap

逻辑说明:这一段是 beta cap 的实现逻辑。当组合加权 beta 超过设定上限时,不对个股做调整,而是直接缩仓——把多头权重按比例缩小,空头规模也缩到上限对应值。这样不会改变组合内个股的相对权重,只降低整体市场暴露。参数beta_cap=1.0是源码图表里的默认值,你可以改成 0.5 或 2.0 观察组合波动率的变化。

4.3 月度调仓的回测循环:注意交易成本和调仓频率

月度调仓的回测框架,核心是一个循环:每个月末生成预测 → 筛选股票 → 调仓 → 记录组合收益 → 更新下个月的持仓。源码里的图表显示组合月收益分布和累计净值曲线,说明回测是完整跑通的。但有一个很容易忽略的坑——交易成本。

月度调仓一年只有 12 次,成本压力比日频小很多,但做多一半股票、做空指数,每次调仓涉及的换手率可能达到 40% 到 60%。如果忽略交易成本,年化收益可能虚高 3 到 5 个百分点。我一般会在回测里计入双边千分之二到千分之三的成本。

turnover = (new_weights - old_weights).abs().sum() cost = turnover * 0.002 # 双边成本 0.2% net_return = gross_return - cost # 月收益序列记录 monthly_returns.append(net_return)

逻辑说明:turnover用新旧权重的绝对差求和得到,代表组合中发生变动的仓位比例。成本率取 0.2% 是 A 股和美股都相对合理的近似值,如果你回测的是小盘股或低流动性股票,建议提到 0.5%。这一步容易被新手漏掉,但它是回测结果可信度的关键。

5. 避坑与常见问题:复现这份源码最容易踩的六个坑

5.1 现象:Quandl 数据拉取失败,报错提示 api key 无效或限流

原因:Quandl 被 Nasdaq 收购后,老接口的免费 api key 大部分失效了,而且免费层有每分钟请求次数限制,循环拉取多只股票时很容易触发限流。

解决:先检查 api key 是否还有效,有效的话在拉取循环里加time.sleep(1)控制请求频率。如果接口已经废弃,换成 Tiingo 或者直接用源码自带的 CSV 缓存文件。注意换数据源后,日期字段要统一成datetime64类型,ticker 要统一成大写字幕。

5.2 现象:LSTM 训练时 loss 下降很慢,甚至不下降

原因:最常见的是特征没有做标准化。LSTM 内部的 sigmoid 和 tanh 激活函数对输入尺度敏感,如果 PE 是几十倍而动量是 0.1,梯度会被大数值特征主导,训练不稳定。

解决:对所有特征做 z-score 标准化,特别是进入 LSTM 之前必须有独立的 scaler。不要用全局 scaler,要在训练集上 fit,再 transform 验证集和测试集,防止数据泄露。

5.3 现象:回测收益很高,但实盘完全跑不出

原因:这是量化里最经典的坑——前视偏差。可能性有三个:一是特征里用了未来数据,比如用当月的完整数据预测当月收益;二是没有做 survivorship bias 处理,回测池里只有今天还活着的股票;三是回测没有扣交易成本。

解决:检查标签构造的shift是否正确,确认回测的股票池是当时实际可交易的。最简单的验证办法是:把回测时间往前拨一年,看模型是否还能稳定盈利。如果不能,基本可以断定有前视偏差。

5.4 现象:逻辑回归和梯度提升的预测结果几乎一样

原因:特征池可能只有线性可分的信息,或者特征之间的交互很弱。梯度提升的优势在非线性交互,如果特征本身就是线性关系,两个模型学到的决策边界确实高度相似。

解决:这不是 bug,但说明特征工程做浅了。尝试加入更多交互特征,比如 PE 和营收增速的比值(PEG),或者 ROE 的同比变化。情绪特征也是一个突破口,新闻情感打分对逻辑回归的增量贡献通常更明显。

5.5 现象:月度调仓时,某些月份完全没有可交易的股票

原因:筛选前 20% 时如果市场整体处于低迷期,模型可能预测大部分股票下月收益为负,导致通过筛选的股票数量不足,组合无法分散。

解决:在筛选逻辑里加一个最低持仓数量限制,比如至少持有 20 只。如果符合条件的股票不够,就放宽到前 30% 或者持有现金。注意这会让策略的 beta 暴露变大,需要重新评估市场中性效果。

5.6 现象:LSTM 的验证集准确率一直停在 50% 左右

原因:月度金融数据的信噪比极低,LSTM 在这种数据上很难学到稳定的时序模式。如果时间步长取 12,而有效样本只有几百,模型容量和样本量严重不匹配。

解决:把时间步长缩短到 6,隐藏层维度降到 16,dropout 加到 0.5。如果验证集准确率还是不上 52%,说明这个数据集不适合 LSTM,直接用梯度提升的结果就好。源码里的对比图表应该也有展示,不含情绪特征时 LSTM 的优势会明显缩小。

6. 进阶验证:怎么判断模型是真有效还是过拟合

模型跑通只是第一步,更重要的验证是区分真实的预测能力和随机噪音。我每次跑完这个策略,会强制走一遍三个验证步骤,几分钟就能暴露大部分过拟合问题。

第一步是时间序列的样本外测试。不能用随机 K 折交叉验证,金融数据必须按时间切分——比如 2015 到 2019 年做训练,2020 到 2023 年做验证。如果训练集和验证集有重叠月,模型等于开卷考试。针对这个资源,我会在 Notebook 里把TimeSeriesSplit换成手动切分,确保训练数据的时间截止点严格早于验证数据。

from sklearn.model_selection import TimeSeriesSplit tscv = TimeSeriesSplit(n_splits=5) for train_idx, val_idx in tscv.split(X): X_train, X_val = X.iloc[train_idx], X.iloc[val_idx] # 训练和验证

第二步是预测结果的置信区间分析。逻辑回归可以输出预测概率,你要看预测概率在 0.6 以上的股票,实际上涨比例是否显著高于预测概率在 0.5 附近的股票。如果两者差不多,说明模型的预测概率是校准不良的,高概率不代表高胜率。

# 按预测概率分箱,验证校准度 pred_df = pd.DataFrame({"prob": y_pred_prob, "actual": y_test}) calibration = pred_df.groupby(pd.qcut(pred_df["prob"], 5)).agg( mean_prob=("prob", "mean"), actual_rate=("actual", "mean") ) print(calibration)

这段代码把预测概率分成五档,每档取平均预测概率和实际正例占比。理想情况下两者应该接近——第一档实际正例率 50%,第五档 60% 到 70%。如果第五档实际正例率不到 55%,模型的排序能力就没有转化为选股胜率,策略的 alpha 来源值得怀疑。参数注意qcut是按分位数切分的,如果概率分布集中在中段,分箱后各箱的样本量会不均衡。

第三步是最关键的换手率分析。真实策略的月度调仓里,每个月选出的前 20% 股票是高度重叠的——好公司的特征不会一个月内彻底变坏。如果模型每个月选出的股票重合率低于 30%,说明模型在追逐短期噪音,调仓成本会吃掉收益。观察重合率并画出来,能看到模型预测的稳定性。

# 计算相邻月份选股重合率 overlap = set(month1_top20).intersection(set(month2_top20)) overlap_rate = len(overlap) / len(month1_top20)

如果 overlap 率很低,我会考虑给预测概率加一个平滑处理,比如用过去三个月的平均概率作为选股依据。这能显著降低换手率,也顺便提高了预测的稳定胜率。源码里的图表不包含这个分析,你可以自己在 Notebook 末尾追加这段代码。

最后一个实用技巧:把逻辑回归的系数打印出来,看哪些特征的权重最大。如果动量因子权重过高,说明策略本质是在做动量而不是基本面选股;如果估值因子权重高,策略会更接近价值投资风格。看完系数之后,你可以决定是否要调整特征池。从那以后我每次跑完这个策略,都会把这三步验证的图表存下来作为交付的一部分,这东西比得到一组回测净值数字有价值得多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询