简介:本资源为基于机器学习的股票量化投资研究算法源码包,面向计算机、人工智能、大数据、数学及电子信息等专业的学生与量化技术学习者,可用于课程设计、期末大作业或毕业设计,也可作为理解机器学习在金融商贸场景落地的参考案例。压缩包共14个文件,约30.29MB,以6个Python脚本和6个CSV数据文件为主,另含少量配置文件;脚本覆盖数据获取、模型训练、交易模拟与主流程调度,CSV则提供单只及多只股票的历史行情数据,便于直接复现实验。项目按单只股票预测与多只股票预测两条主线组织,包含数据抓取、特征处理、训练与回测等模块,结构清晰,适合在已有基础上调试与二次开发。目前已有210人学习关注,具备一定参考热度。对于希望把机器学习方法应用到股票量化策略、并需要完整可运行代码与数据支撑的读者,这份资源能提供从数据到交易信号的实践路径与排错思路。
1. 从一份「算法源码+项目说明」压缩包说起:机器学习做股票量化到底交付什么
很多人第一次拿到「基于机器学习的股票量化投资研究算法源码+项目说明.zip」这类东西,第一反应是解压、找main.py、直接python main.py,然后盯着屏幕等收益曲线。结果大概率是报错、缺依赖、数据路径不存在,或者跑出来一条漂亮到不真实的净值曲线。问题不在代码,在于没搞清楚这类项目到底交付了什么。
它交付的不是一个能直接下单赚钱的策略,而是一套研究框架:数据怎么组织、特征怎么算、标签怎么定义、模型怎么训练、回测怎么避免未来函数。机器学习在股票量化里的角色,是把「人写规则」换成「模型从历史数据里找规律」,但规律能不能用,取决于数据管道和验证方式,而不是模型本身有多深。这份东西适合两类人:想入门量化但不知道怎么把机器学习接进来的开发者,以及写过策略但回测总是虚高的从业者。下面按「先立住原理、再动手复现、最后讲坑」的顺序拆开。
2. 数据管道与特征工程:量化研究的胜负手在模型之前
2.1 为什么股票量化的数据不能直接喂给模型
股票数据有三个和常规机器学习数据集完全不同的性质。第一是时序性,今天的收益和昨天相关,不能随机打乱做交叉验证,否则就是用未来预测过去。第二是低信噪比,日频收益里能被预测的部分通常不到百分之一,模型很容易学到噪声。第三是非平稳,市场结构会变,三年前有效的因子今年可能失效。
所以这类项目里,数据管道的设计比模型选型重要得多。常见做法是把原始行情(开高低收、成交量、成交额)先落成一张按日期和股票代码索引的表,再做特征。特征大致分几类:动量类(过去 N 日收益率)、波动类(过去 N 日收益标准差)、量价类(换手率、量比)、以及横截面排名类(把每个因子在当天所有股票里做百分位排名)。横截面排名这一步很关键,它把绝对数值转成相对强弱,能显著降低非平稳带来的影响。
2.2 用 pandas 搭一个可复现的特征计算脚本
下面这段代码是这类项目里最常见的特征生成骨架,我一般会把它单独放成features.py,输入是长表格式的行情数据,输出是带标签的宽表。
import pandas as pd import numpy as np def build_features(df, windows=(5, 10, 20), label_horizon=5): """ df: 长表,列包含 [date, code, open, high, low, close, volume] windows: 动量与波动的回看窗口 label_horizon: 标签用未来多少天的收益 """ df = df.sort_values(["code", "date"]).copy() g = df.groupby("code", group_keys=False) # 1. 基础收益率 df["ret1"] = g["close"].pct_change() # 2. 动量特征:过去 N 日累计收益 for w in windows: df[f"mom_{w}"] = g["close"].pct_change(w) # 3. 波动特征:过去 N 日收益标准差 for w in windows: df[f"vol_{w}"] = g["ret1"].transform(lambda s: s.rolling(w).std()) # 4. 量价特征:换手率近似(成交量 / 20日均量) df["vol_ratio"] = df["volume"] / g["volume"].transform( lambda s: s.rolling(20).mean() ) # 5. 标签:未来 label_horizon 日收益,注意 shift 方向 df["label"] = g["close"].transform( lambda s: s.shift(-label_horizon) / s - 1 ) # 6. 横截面排名:每个交易日对所有股票做百分位 feat_cols = [c for c in df.columns if c.startswith(("mom_", "vol_", "vol_ratio"))] for c in feat_cols: df[c + "_rank"] = df.groupby("date")[c].rank(pct=True) return df.dropna(subset=["label"])逻辑说明:先按code和date排序,保证所有滚动计算都在单只股票内部进行,不会串到别的股票。pct_change(w)算的是过去 w 日的累计收益,rolling(w).std()算波动。标签用shift(-label_horizon),意思是「今天买入、持有 label_horizon 天后卖出」的收益,方向不能写反,写反了就是未来函数。
参数说明:windows一般取 5、10、20、60,覆盖短中周期;label_horizon取 1 到 10,取太长会让样本量骤减且标签噪声更大。vol_ratio里的 20 日均量是经验值,换成 10 或 30 影响不大,但要在项目说明里固定下来,方便复现。
提示:
dropna(subset=["label"])会丢掉每只股票最后 label_horizon 天的数据,这是正常的,因为那几天没有未来收益可算。不要为了「保留更多样本」把标签用当日收益代替,那是自欺欺人。
2.3 数据切分:时间序列不能随机分
常规机器学习用train_test_split随机切,量化里绝对不行。正确做法是按时间切:前 70% 做训练,中间 15% 做验证调参,最后 15% 做样本外测试。更严格一点用滚动窗口:每次用过去 N 年训练、下一年测试,然后窗口往前滚。这类项目的说明文档里如果写了「随机划分数据集」,基本可以判定回测结果不可信。
3. 模型选型与训练:从线性回归到梯度提升,别一上来就上深度学习
3.1 为什么量化里树模型比神经网络更常见
股票特征大多是表格型数据,样本量在几千到几十万行之间,特征维度几十到几百。这个规模下,梯度提升树(LightGBM、XGBoost)通常比神经网络表现更稳,训练更快,对特征缩放不敏感,还能直接输出特征重要性。神经网络在图像、文本上强,是因为那些数据有局部相关性和层次结构,表格数据没有这种先验。
所以这类项目里,合理的模型梯队是:线性回归(Ridge/Lasso)做基线,LightGBM 做主模型,神经网络作为对比实验。如果项目说明里只有 LSTM 或 Transformer,而没有树模型基线,要么是研究性质,要么是没做过对比。
3.2 LightGBM 训练与时间序列交叉验证的最小代码
import lightgbm as lgb import numpy as np from sklearn.metrics import mean_squared_error def time_series_cv(df, feat_cols, n_splits=5): """按时间滚动做交叉验证,返回每折的验证集 IC 和 RMSE""" dates = np.sort(df["date"].unique()) fold_size = len(dates) // (n_splits + 1) results = [] for i in range(n_splits): train_end = dates[fold_size * (i + 1)] val_end = dates[fold_size * (i + 2)] if i + 2 <= n_splits else dates[-1] train = df[df["date"] <= train_end] val = df[(df["date"] > train_end) & (df["date"] <= val_end)] model = lgb.LGBMRegressor( n_estimators=500, learning_rate=0.03, num_leaves=31, min_child_samples=100, subsample=0.8, colsample_bytree=0.8, reg_alpha=0.1, reg_lambda=0.1, random_state=42, ) model.fit( train[feat_cols], train["label"], eval_set=[(val[feat_cols], val["label"])], eval_metric="rmse", callbacks=[lgb.early_stopping(50, verbose=False)], ) pred = model.predict(val[feat_cols]) ic = np.corrcoef(pred, val["label"])[0, 1] rmse = mean_squared_error(val["label"], pred, squared=False) results.append({"fold": i, "ic": ic, "rmse": rmse, "n_val": len(val)}) return results逻辑说明:time_series_cv把时间轴切成 n_splits+1 段,每次用前面所有数据训练、下一段验证,模拟真实交易中「只能用历史数据」的约束。early_stopping(50)在验证集 RMSE 连续 50 轮不下降时停,防止过拟合。
参数说明:learning_rate=0.03配合n_estimators=500是保守配置,学习率越低需要越多树,但泛化更好。min_child_samples=100是防过拟合的关键,量化数据噪声大,叶子节点样本太少会记住噪声。subsample和colsample_bytree都设 0.8,增加随机性。reg_alpha和reg_lambda是 L1/L2 正则,量化里通常都要加。
评价指标里IC(信息系数)比 RMSE 更重要。IC 是预测值和真实收益的横截面相关系数,IC 均值 0.03 到 0.05 在日频上就算可用,0.1 以上要怀疑有未来函数。RMSE 只反映数值拟合,不反映排序能力,而选股靠的是排序。
3.3 特征重要性与因子筛选
训练完看model.feature_importances_,但别直接按重要性删特征。树模型的重要性会偏向高基数特征,而且相关特征之间会互相稀释。更稳的做法是看分组重要性:把特征按类别(动量、波动、量价)分组,看每组总重要性。如果某一组几乎为零,说明这类因子在当前数据上没信息,可以考虑去掉。另外,特征数量控制在 30 到 80 之间比较合适,太多会稀释信号,太少覆盖不全。
4. 回测与绩效评估:为什么你的净值曲线是假的
4.1 回测里最常见的三个未来函数
第一个是用当日收盘价买入。如果你用当天收盘后算出的特征,又假设当天收盘价成交,那等于用收盘信息在收盘前交易。正确做法是 T 日收盘算特征,T+1 日开盘或收盘成交。第二个是用全样本做标准化。把整个时间段的均值和标准差拿来标准化特征,用到了未来信息。标准化参数必须只用训练集算。第三个是幸存者偏差。只拿现在还在上市的股票回测,退市的那些没算进去,收益会被高估。
4.2 一个最小回测循环
def backtest(df, pred_col, top_n=50, cost=0.001): """每日选预测值最高的 top_n 只,等权持有,次日收益""" df = df.sort_values(["date", "code"]).copy() df["rank"] = df.groupby("date")[pred_col].rank(ascending=False) df["position"] = (df["rank"] <= top_n).astype(float) df["position"] = df.groupby("date")["position"].transform( lambda s: s / s.sum() ) # 次日收益:用 label 的日频版本,这里假设 label 是持有期收益 df["strategy_ret"] = df["position"] * df["ret1"].shift(-1) daily = df.groupby("date")["strategy_ret"].sum() # 扣交易成本:换手率近似为每日持仓变化 turnover = df.groupby("date")["position"].apply( lambda s: np.abs(s.diff()).sum() / 2 ) daily = daily - turnover * cost cum = (1 + daily).cumprod() return daily, cum逻辑说明:每天按预测值排序,取前 top_n 只等权。ret1.shift(-1)是次日收益,对应 T 日选股、T+1 日持有。换手率用持仓权重的绝对变化除以 2 估算,乘以单边成本得到每日成本。
参数说明:top_n取 30 到 100,太少波动大,太多接近指数。cost单边取 0.001 到 0.003,包含佣金和冲击成本,A 股还要考虑印花税。如果回测里cost=0,那结果只能当上限看。
4.3 看哪些指标,不看哪些
必看:年化收益、年化波动、夏普比率、最大回撤、换手率、IC 均值、ICIR(IC 均值除以 IC 标准差)。夏普在日频上 1.0 以上算不错,2.0 以上要警惕。ICIR 大于 0.3 说明预测稳定性尚可。不要只看累计收益曲线,那条线可以靠少数几笔极端盈利拉起来,要看收益分布和回撤持续期。
5. 避坑与排查:跑这类项目最容易翻车的五个地方
5.1 现象:回测夏普 3.0 以上,实盘完全不行
原因:大概率有未来函数,最常见的是标准化用了全样本,或者标签和特征的时间对齐错了。解决:把特征计算和标签计算的时间戳打印出来,逐行核对。用「T 日特征 + T+1 日收益」的规则重新对齐,标准化参数只用训练集重算。
5.2 现象:训练集 IC 0.2,验证集 IC 0.01
原因:过拟合。特征太多、树太深、叶子样本太少都会导致。解决:把num_leaves降到 15 到 31,min_child_samples提到 100 以上,加reg_alpha和reg_lambda,减少特征数量到 50 以内。如果还不行,说明特征本身没信息,换因子。
5.3 现象:换手率极高,扣成本后收益归零
原因:预测值日间波动大,每天持仓大换血。解决:对预测值做平滑(比如 5 日均值),或者加持仓约束(单只股票权重上限、换手率上限)。也可以在训练时把换手惩罚加进目标函数,但实现复杂,先用平滑更实际。
5.4 现象:某些股票代码在特征里是 NaN,模型报错
原因:新股上市初期没有历史数据,滚动窗口算不出来。解决:在特征生成后统一dropna,或者对缺失值做横截面填充(用当天同行业均值)。不要用前向填充,那会引入未来信息。
5.5 现象:同一份代码两次运行结果不一样
原因:LightGBM 默认多线程,浮点求和顺序不同会导致微小差异,加上随机种子没固定。解决:设random_state=42,deterministic=True,num_threads=1做最终复现。日常实验可以多线程,但出报告时要固定。
6. 把研究框架变成可复用工具:三个我常用的进阶技巧
第一个技巧是把特征计算和模型训练解耦成配置文件驱动。用一个 YAML 描述用哪些因子、窗口多长、模型参数是什么,代码只读配置。这样换因子不用改代码,实验记录也可追溯。我一般会在项目根目录放config/features.yaml和config/model.yaml,每次实验存一份快照。
第二个技巧是用 IC 衰减曲线判断因子有效期。算每个因子在不同持有期(1 天、5 天、10 天、20 天)的 IC,如果 IC 随持有期快速衰减,说明是短期因子,适合高频调仓;如果衰减慢,适合低频。这个曲线比单点 IC 更能说明因子的性质。
第三个技巧是样本外跟踪而不是一次性回测。把最后一段样本外数据按季度切开,看每个季度的 IC 和收益,如果某些季度特别差,去查那段时间的市场状态(波动率、风格切换)。这能帮你判断策略在什么环境下会失效,而不是等实盘亏了才知道。
| 检查项 | 合格线 | 危险信号 |
|---|---|---|
| 样本外 IC 均值 | > 0.02 | < 0 或 > 0.15 |
| ICIR | > 0.3 | < 0.1 |
| 年化换手率 | 5 到 20 倍 | > 50 倍 |
| 最大回撤 | < 30% | > 50% |
| 扣费后夏普 | > 0.8 | > 2.5 |
这张表是我自己每次出报告前会过一遍的清单,超过危险线的先怀疑数据问题,再怀疑模型。最后说个血泪经验:我早期做这类项目,最常犯的错不是模型选错,而是急着看收益曲线,跳过了数据对齐的检查。后来养成习惯,任何新特征上线前先跑一遍「特征与未来收益的 IC 是否显著」,不显著就不进模型。这个习惯帮我省了很多后悔药。希望帮到你。
本文还有配套的精品资源,点击获取