简介:本资源为Python机器学习驱动的基本面量化投资研究项目,包含完整源码、配套数据集与项目说明文档,面向具备一定Python基础、希望将机器学习算法应用于金融量化场景的学习者和研究者。包内共219个文件,主要由167个csv数据文件、36个abak中间结果文件、11个py源码脚本及pdf、md说明文档组成,涵盖数据预处理、模型训练、策略结果分析等环节;压缩包整体约145.79MB,可直接运行,便于复现和扩展实验。目前已有49人学习下载。项目集成线性回归、随机森林、XGBoost、LSTM、GBDT、Ridge、Lasso、ElasticNet等常用算法,通过实际案例演示模型选择、调参对比和结果评估流程,并附有算法对比结果文件,便于直观理解不同模型在量化投资中的适用性。对入门者可作为建立量化投资基本流程的起点,对有基础用户则可用于深入理解机器学习算法在基本面分析、组合评估及策略构建中的实际应用与优化思路。
1. 机器学习+量化投资研究项目:先确认你要解决的是预测还是流程
很多人在接触Python之后,会走到同一个岔路口:手上有行情数据,想用机器学习找规律,却不知道从哪一步开始。这个标题指向的项目“机器学习驱动的量化投资研究,集成多种算法”,不是一个给你“稳赚信号”的代码包,而是一条标准研究链路:把数据集整理成监督学习格式、用多种算法做预测、用回测检验结果。适合有Python和pandas基础、想系统做量化研究的从业者,也适合想把自己手工指标升级成“可验证策略”的交易者。它的真正价值在于,让研究假设变成可复现、可对比、可反驳的实验流程,而不是一个只存在于截图里的漂亮曲线。
2. 数据集与特征工程:把行情表改造成监督学习训练集的落地步骤
如果你打开这份研究项目源码,最先看到的往往不是模型,而是一堆CSV或Parquet文件。这个顺序是对的:量化研究里,数据决定上限,模型只是逼近这个上限。所以我一般会把“数据集与特征工程”放在第一章来讲,先把数据管道的几个坑填平,后面跑模型才不闹鬼。
2.1 数据集字段标准化:先解决对齐,再看模型
市面上能拿到的行情数据,无论是免费接口还是商业数据源,字段名都五花八门,但核心始终是这几列:交易时间、开盘价、最高价、最低价、收盘价、成交量、成交额。这份项目源码里的数据集,大概率已经做了字段重命名,但你自己从别处下载新数据时,一定会遇到列名不一致的问题。
常见做法是,把字段统一成下面这套约定,再进特征工程:
| 约定字段 | 含义 | 常见坑 |
|---|---|---|
| ts_code | 股票/合约代码 | 注意是字符串,不是数字 |
| trade_date | 交易日期 | 要parse成datetime,不要当字符串排序 |
| open / high / low / close | OHLC价格 | 必须统一复权口径,混用会算错收益 |
| vol / amount | 成交量 / 成交额 | vol单位可能是手也可能是股 |
行情数据的第一个坑,就是复权。前复权、后复权、不复权三种口径算出来的收益率完全不一样。我的习惯是:用后复权价格计算动量类因子和标签,因为后复权不会因为除权除息造成收益率跳变;回测引擎里下单时再切回前复权价格做撮合。记住一句话:全项目统一用一种复权口径,中途不要混。
数据预处理还要做三件脏活。第一,剔除上市不满60个交易日的次新股,次新股的涨跌幅和流动性都不正常。第二,识别并处理停牌日,停牌期间成交量为0,收益率是NaN,直接drop掉或前向填充要看场景。第三,过滤ST和涨跌停样本,这个后面避坑章节会单独讲。
2.2 预测目标与特征:标签怎么定,特征怎么来
数据清洗完,下一步是把“预测涨跌”翻译成监督学习的输入输出。很多新手拿到数据直接丢给LightGBM,结果模型把公司代码当成特征学进去了,这就是没有理解“标签”的含义。
标签定义是这个项目最关键也最容易返工的地方。常见做法是,用未来N个交易日的收益率作为预测目标,然后转成二分类或者回归。比如预测未来5日收益率是否大于等于5%,大于等于就标1,否则标0。这个阈值直接影响正负样本比例:阈值设太低,正例太多模型学不到区分度;设太高,正例太少,模型会偏向全部预测0。
我一般这样构造特征:
- 动量类:过去5日、10日、20日收益率,刻画短中期趋势
- 波动类:过去20日日收益率的标准差,刻画风险水平
- 量能类:当日成交量与过去5日平均成交量的比值,刻画量能异动
- 价量背离类:价格创新高但成交量没跟上,这类结构特征有时比纯价格特征有效
特征构造的原则只有一条:任何特征在时间t只能使用t时刻及之前的信息。这是量化机器学习里最根本的约束,违反它就是未来函数,后面章节细说。
2.3 可复现的数据预处理脚本
在动手写模型之前,先把数据管道跑通。下面这段代码是我处理日线数据的基础脚本,能在pandas里完成排序、特征计算和标签生成。
import pandas as pd import numpy as np # 读入日线数据,约定字段名见 2.1 节的表 df = pd.read_csv("daily_data.csv", parse_dates=["trade_date"]) df = df.sort_values(["ts_code", "trade_date"]).reset_index(drop=True) def build_dataset(df: pd.DataFrame, label_days: int = 5, ret_threshold: float = 0.05, min_periods: int = 20): # 日收益率:注意 groupby 后算,不要跨股票算 df["ret_1d"] = df.groupby("ts_code")["close"].pct_change() # 动量因子:过去 5 / 10 / 20 日累计收益 for w in [5, 10, 20]: df[f"momentum_{w}"] = ( df.groupby("ts_code")["close"].transform(lambda x: x.pct_change(w)) ) # 波动因子:过去 20 日收益率标准差 df["volatility_20"] = ( df.groupby("ts_code")["ret_1d"] .transform(lambda x: x.rolling(min_periods, min_periods).std()) ) # 量能因子:当日成交量 / 过去 5 日均量 df["vol_ma5"] = ( df.groupby("ts_code")["vol"].transform( lambda x: x.rolling(5, 5).mean() ) ) df["volume_ratio"] = df["vol"] / df["vol_ma5"] # 标签:未来 label_days 日收益,shift 到当前行 df["label_ret"] = ( df.groupby("ts_code")["close"] .transform(lambda x: x.shift(-label_days) / x - 1.0) ) df["label"] = (df["label_ret"] >= ret_threshold).astype(int) # 清理无穷值和没有标签的尾部样本 df = df.replace([np.inf, -np.inf], np.nan) df = df.dropna(subset=["label", "volatility_20"]).reset_index(drop=True) return df sample = build_dataset(df, label_days=5, ret_threshold=0.05) print(sample[["ts_code", "trade_date", "momentum_5", "volume_ratio", "label"]].head())label_days是预测周期,代表你持有多久再评估收益,一般取1、5、10、20,和交易频率强相关。ret_threshold是正例阈值,建议先看label_ret的分位数再定,而不是拍脑袋设0.05。min_periods控制滚动窗口最少样本数,上市时间短的股票滚动20日标准差会算出NaN,这里设为20意味着不足20个样本直接置空。
这段代码里有两点要注意:pct_change自带组内结果,但rolling需要和groupby搭配,且rolling结束后要用reset_index或transform对齐索引,否则会出现错位。另一个细节是dropna(subset=["volatility_20"])会把上市不满20天的新股全部剔除,这是有意的。
2.4 特征泄漏:那个看起来完美的回测为何一上实盘就翻车
特征泄漏是量化机器学习里最常见的回测翻车原因,而且它藏得极深。最典型的场景是:你想填充缺失值,直接对整个数据集做了fillna(df.mean()),这个均值是全样本的,包含了未来信息,模型在训练时“偷看”了未来分布。同样的道理,如果你用标准化StandardScaler().fit(X)时先拟合了整个X,再切训练测试集,也会泄漏。
解决方法一是把fit限制在训练集上,测试集只用transform;二是所有因子计算都按 2.3 节的写法,在groupby内部完成,不让未来数据跨行流动。
另一个高频泄漏源是shift方向搞反。计算未来收益作为标签,要用shift(-label_days)把未来收益挪到当前行;而计算特征时,如果用了未来N日的数据做平均,就是往过去“偷”数据。一个直观的检查方法:把特征和标签画在同一张图上,看特征在时间轴上是否超前于标签变化。超前就是泄漏,趁早删。
3. 多算法集成:用组合模型代替单一模型,参数与切分方式怎么定
数据管道跑通后,进入标题里最核心的部分——集成多种算法。为什么研究项目要强调“多种算法”而不是“调出一个最强模型”?因为金融数据信噪比极低,任何单一模型都容易过拟合到某段行情的噪声里,集成至少能把“只看一个方向”的风险摊薄。
3.1 算法对比:为什么多算法集成是量化研究的默认做法
先看一张在日频A股数据上常见的模型表现对比,结论是“没有绝对最强,只有更稳”:
| 模型 | 优势 | 劣势 | 适合场景 |
|---|---|---|---|
| 逻辑回归 | 稳定、可解释、不容易过拟合 | 只能学线性关系 | 基线模型,必跑 |
| 随机森林 | 能抓非线性,对异常值钝感 | 对噪声敏感,容易过拟合 | 中等维度特征 |
| GBDT/LightGBM | 表格数据王者,特征交互强 | 参数多,调不好就是过拟合王 | 特征工程到位后的主力 |
| SVM | 小样本高维表现好 | 核函数选择玄学 | 特征维度高但样本不多的阶段 |
| MLP | 能学复杂映射 | 对数据量和调参要求高 | 特征已经非常干净的阶段 |
我在实际项目里的做法是:逻辑回归永远作为基线,随机森林和GBDT作为主力,SVM看情况上。跑完一轮后,把预测结果做等权平均,AUC通常比任何单一模型都稳。这里有个反直觉的经验:集成后AUC不会暴涨,但回测曲线的回撤会变小,这才是集成在量化里的价值。
3.2 时序交叉验证:K折在这里不适用
这是机器学习在量化里最容易被忽略的一步。KFold默认会打乱样本顺序随机切分,这在图像分类里没问题,但在时序数据里就是灾难:模型用2023年的数据训练,却在2020年的数据上验证,相当于让模型“预知未来”。
正确做法是用TimeSeriesSplit,它严格按时间顺序切分,训练集永远在验证集之前。下面这个时序交叉验证的拆法,能保证每一折都不含未来信息:
from sklearn.model_selection import TimeSeriesSplit tscv = TimeSeriesSplit(n_splits=5) for fold, (train_idx, val_idx) in enumerate(tscv.split(X)): X_train, X_val = X[train_idx], X[val_idx] y_train, y_val = y[train_idx], y[val_idx] print(f"fold {fold + 1}: train {len(train_idx)} samples, val {len(val_idx)} samples")注意,TimeSeriesSplit的切分是等比扩窗:训练集不断变大,验证集固定为一段。第一次切分时训练集可能只有1/5的样本,模型会欠拟合,所以一般我只看最后一个fold的结果做决策,前面的fold用来观察模型是否随数据量增加而进步。
还有一个细节:金融数据相邻交易日高度自相关,紧挨着切分会造成“训练集最后一天和验证集第一天”之间存在信息重叠。我习惯在训练集尾部留出5到10个交易日的gap再开始验证集,这个gap窗口完全不参与训练,能显著降低时序泄漏。
3.3 集成训练最小脚本
特征和标签准备好之后,下面是一份可以直接跑的集成训练脚本,它把逻辑回归、随机森林、GBDT三个模型的结果做了等权平均,再用AUC评估。
from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier from sklearn.metrics import roc_auc_score import numpy as np # 取特征列:只保留以 momentum_ / volatility_ / volume_ 开头的列 feature_cols = [ c for c in sample.columns if c.startswith(("momentum_", "volatility_", "volume_")) ] X = sample[feature_cols].fillna(0.0).values y = sample["label"].values # 用最后一个 fold 做验证 split_point = int(len(X) * 0.8) X_train, X_val = X[:split_point], X[split_point:] y_train, y_val = y[:split_point], y[split_point:] models = { "lr": LogisticRegression(max_iter=1000, C=1.0), "rf": RandomForestClassifier( n_estimators=200, max_depth=5, min_samples_leaf=50, random_state=42 ), "gbdt": GradientBoostingClassifier( n_estimators=200, max_depth=3, learning_rate=0.05, random_state=42 ), } proba_list = [] for name, model in models.items(): model.fit(X_train, y_train) proba = model.predict_proba(X_val)[:, 1] proba_list.append(proba) auc = roc_auc_score(y_val, proba) print(f"{name} AUC: {auc:.4f}") # 等权平均集成 ensemble_proba = np.mean(proba_list, axis=0) ensemble_auc = roc_auc_score(y_val, ensemble_proba) print(f"ensemble AUC: {ensemble_auc:.4f}")为什么max_depth要设这么小?因为金融数据噪声大,树一旦长得深就疯狂拟合噪声,样本外性能直线下降。min_samples_leaf=50强制每个叶子至少50个样本,相当于给树加了一根“稳健性保险丝”。GBDT的learning_rate=0.05配合浅树,比大学习率加深度更容易收敛到稳定区域。
等权平均是默认选项,逻辑上比投票法更平滑,因为predict_proba输出的概率保留了置信度信息。投票法只取硬标签,一旦某两个模型同时出错,结果就会错得更离谱。等权平均能部分对冲这种“共识性错误”。
3.4 调权重的边界:别把验证集变成测试集
等权平均是没问题的基线,但你一定想把权重调一调,比如逻辑回归0.2、随机森林0.3、GBDT0.5。可以这么做:在验证集上做一次网格搜索,选出让AUC最大的权重组合。
但这里有一条血泪经验:权重只能在验证集上调,绝对不能看到测试集后再回头调权重。否则权重会过拟合到测试集的某一段行情里,样本外表现立刻打回原形。我见过有人拿同一份数据反复调权重,调出0.7的AUC,换一段新数据直接掉到0.52,这就是把验证集当训练集反复薅的后果。
更稳妥的做法是:只调“算法层”的权重,不调“参数层”的权重。换句话说,每个模型的内部参数保持默认或按经验固定,权重搜索范围放在0.2到0.6之间,步长0.1。搜索空间越小,过拟合风险越低,项目的可维护性越高。
4. 回测避坑实录:收益曲线好看背后的五个常见翻车点
模型训练完毕,AUC再高也只是预测能力,离“能赚钱的策略”之间还隔着一道回测。这一章直接把研究项目里最常踩的坑列出来,每一条都是真实项目里反复出现的现象、原因和解决方式。
4.1 前视偏差:回测中最常见的未来函数
现象:回测曲线完美,年化收益高得吓人,但一上实盘或者换一段新数据就崩。
原因:特征计算或信号生成时,无意中用到了未来数据。最典型的就是没有对因子做shift(1),导致“当天收盘后计算的因子”被当成“当天开盘就能交易的信号”去撮合。实际上,一个用收盘价计算的因子,最早只能在第二天开盘执行,当天价格已经收盘,根本交易不进去。
解决:所有因子列在进入模型前统一执行df[factor] = df.groupby("ts_code")[factor].shift(1),把整体信号往后推一个交易日。这是量化回测里最便宜也最有效的防未来函数手段。每次写完因子,检查一下:如果因子在t日用到close,那它在t日收盘前是否可知?不可知就shift。
4.2 停牌与涨跌停:价格看得见,但交易不进去
现象:策略回测中重仓的股票连续涨停,净值曲线直线拉升,实盘根本买不进。
原因:数据集里的涨跌停价格是真实存在的,但涨停时买单排队,轮不到你的成交价;跌停时卖单排队,你的卖单也出不去。回测引擎用当天收盘价撮合,默认你总能成交,这就产生了巨大的幸存者偏差。
解决:在数据清洗阶段过滤涨跌停样本,或者在回测撮合时把涨停日的买入信号改为“无法成交”。A股常见做法是:当日收盘价触及涨停价的股票,次日信号直接置空。我在2.1节说的“过滤涨跌停样本”,指的就是这里。成交价也不能用收盘价,一般按次日开盘价加滑点撮合,这样更接近真实冲击成本。
4.3 样本外污染:同一个数据集反复调参等于把验证集当训练集
现象:验证集AUC 0.68,觉得不错,于是换个参数又跑一次,0.71,再换个参数,0.73,最终选定0.73那组,信心满满上实盘,结果AUC只有0.53。
原因:每一次调参都看到验证集的结果,人脑已经在隐式地做优化,验证集实际上变成了训练集的一部分。这个逻辑和机器学习模型过拟合验证集在原理上完全一样,只是“过拟合者”变成了你。
解决:把数据集切成三段——训练集、验证集、测试集。训练集用来拟合模型,验证集用来调参,测试集只允许跑一次,跑完就永久归档,不再回头。如果测试集结果不理想,就回去调参,然后换一个更新的时间段重新构造测试集。这个纪律比任何模型技巧都重要。
4.4 费用模型过于乐观
现象:策略每天换仓,回测年化30%,扣除佣金和滑点后只剩5%,换了实盘更低。
原因:量化研究项目源码里如果只写了佣金万2.5,那就忽略了三个隐藏成本:印花税(卖出单边收取)、最低佣金5元、滑点。高频换仓策略对费用极度敏感,换手率高达日均50%时,哪怕万2.5的佣金也会把利润吃掉大半。
解决:回测脚本里至少加上这两行成本:
commission_rate = 0.00025 # 单边佣金万2.5 min_commission = 5.0 # 每笔最低5元 stamp_tax = 0.001 # 卖出单边印花税千1 slippage = 0.002 # 滑点按 0.2% 计算 def calc_trade_cost(trade_value, is_sell=False): commission = max(trade_value * commission_rate, min_commission) tax = trade_value * stamp_tax if is_sell else 0.0 slippage_cost = trade_value * slippage return commission + tax + slippage_costtrade_value是单笔成交金额,is_sell表示卖出方向。滑点设在0.2%已经是比较保守的估计,如果策略交易的是流动性差的小盘股,建议提到0.5%。达不到这个成本门槛的策略,不管AUC多高,都先放一放。
4.5 参数敏感性:换参数就翻车的策略不值得上实盘
现象:n_estimators=100时回测年化25%,改成150变成8%,改回100又变成25%,这个策略在100这个点上表现极好。
原因:模型在某些参数位置上“偶然”契合了样本内的噪声。金融数据的信噪比太低,参数微小变化就可能导致结果大幅波动,说明模型没有抓到稳定的结构,而是在拟合特定时段的运气。
解决:做参数敏感性矩阵,把关键参数在小范围内做网格测试:n_estimators取80、100、120、140,max_depth取3、5、7,记录每个组合的年化收益、回撤、换手率,选的是“邻域内结果稳定”的参数,而不是“单点最优”的参数。我见过太多人在参数精确到个位数的地方发现了“最优参数”,实盘两周就没了,这周一就是量化研究里最常见的玄学之一。
5. 滚动样本外验证:把研究项目变成能持续迭代的最小闭环
最后这一步,是让这份研究项目源码真正脱离“跑一次就扔”的关键。普通回测做完就结束了,但量化研究是一个持续迭代的过程,市场风格一直在变,模型的生命周期往往只有几个月。
滚动样本外验证的做法是:把数据按时间切成多段,每一轮用前面的数据训练,预测下一段,然后扩大训练集再训练,再预测再下一段。这样模拟出来的表现比一次性切分更接近真实上线过程。
n_splits = 5 score_list = [] for i in range(1, n_splits + 1): split_point = int(len(X) * i / (n_splits + 1)) X_train, X_val = X[:split_point], X[split_point:] y_train, y_val = y[:split_point], y[split_point:] model = GradientBoostingClassifier( n_estimators=200, max_depth=3, learning_rate=0.05, random_state=42 ) model.fit(X_train, y_train) auc = roc_auc_score(y_val, model.predict_proba(X_val)[:, 1]) score_list.append(auc) print(f"round {i}: train {len(X_train)}, val {len(X_val)}, AUC {auc:.4f}") print(f"mean rolling AUC: {np.mean(score_list):.4f}")这段代码输出5轮的AUC序列,比单一AUC更有信息量:如果AUC逐渐向上,说明模型正在从数据增长中获益;如果忽高忽低,说明模型不稳定,问题多半出在特征或者参数上;如果一路向下,说明特征失效,市场风格已经切换。
我还有两个习惯。第一个是每次跑实验都把参数和结果记录成一行日志:日期、特征版本、模型参数、AUC、换手率、最大回撤。没有日志的调参过程就是黑匣子,三个月后你再看到一份0.68 AUC的结果,根本想不起来它用了哪套特征。第二是每次迭代只改一个变量,要么加特征,要么改参数,同一次实验改两处以上,出了问题你都不知道该怪谁。
我早期做这个方向时,吃过没做样本外切割的亏,连续两周对着验证集调参,调出来的漂亮曲线一上实盘就哑火。后来养成滚动验证和实验日志两个习惯,项目终于可以安心迭代,不再靠运气说话。这个方向值不值得做,答案是可以做,但前提是先把数据管道、时序切分、成本模型这三件事做扎实。希望帮到你。
本文还有配套的精品资源,点击获取