☰
股票量化投资源码解析:从特征工程到回测避坑的完整流水线
2026/10/1 4:05:38 网站建设 项目流程

简介:这份源码包围绕机器学习在股票量化投资中的应用展开,包含单只股票与多只股票两个预测模块,可用于价格趋势建模、特征分析与交易策略验证。资源面向金融工程、计算机、数据科学等方向的学生,以及正在做课程设计、毕业设计或量化入门实践的学习者;由于完整源码均经过调试,具备一定Python与机器学习基础即可运行并二次开发。

包体共14个文件,以6个Python脚本和6个CSV数据集为主,涵盖数据抓取、训练、预测、交易等环节,压缩包约30.29MB,目录结构清晰。目前已有209人学习下载,适合作为课程项目参考或量化研究起步模板。包内既包含核心代码,也提供可直接使用的股票CSV样本,便于对照实验与结果复现;读者可以借此梳理从原始行情数据清洗、特征构建到模型训练与回测的完整路径,并通过自带样本快速复现单/多只股票的预测流程,节省环境搭建与调试验证时间。

1. 股票量化投资源码包里有什么:一个能跑通的机器学习流水线

我拿到这份“基于机器学习的股票量化投资研究算法源码+项目说明”的时候,第一反应是把它当成又一份“下载即吃灰”的代码包。直到我按项目说明把数据拉下来、特征跑通、模型训练完、回测曲线画出来,才确定它值得写一篇笔记:它不是把几个机器学习算法堆在一个文件里就完事,而是把“股票量化投资”这件事从数据清洗、特征工程、模型训练到回测评估做成了完整流水线,适合想用机器学习做选股研究、又不想从零搭框架的人。你在源码里能看到随机森林、XGBoost这类算法,也能看到项目说明文档里对“为什么这个因子有效”的推导。这套东西把玄学摊开成了代码。如果你已经在做量化但总在特征工程和回测上翻车,这份源码的边界和坑同样值得看一遍。

2. 数据准备与特征构建:给模型的每一列都要有说法

2.1 数据源选择:免费接口和需要 token 的接口怎么取舍

股票量化投资的第一步永远是数据。源码里默认的数据源是 AkShare,选它的原因很直接:免费、不需要注册 token、接口覆盖沪深 A 股和指数成分股。实际你本地的 pandas DataFrame 需要五个字段:日期、开盘、最高、最低、收盘、成交量。拿到 K 线之后,第一件必须做的事是复权处理。你用stock_zh_a_hist拉数据时把adjust参数设为"qfq",也就是前复权。不要小看这一步,很多策略回测好看、实盘拉胯,原因就是除权除息日没有复权,价格出现断崖式跳变,模型把“分红导致的价格下跌”当成了有效信号。

import akshare as ak # 拉取沪深300成分股列表 df_index = ak.index_stock_cons_csindex("000300") codes = df_index["成分券代码"].tolist()[:30] # 先用30只跑通流水线 # 逐只拉取前复权日线数据 price_dict = {} for code in codes: price_df = ak.stock_zh_a_hist( symbol=code, period="daily", start_date="20190101", end_date="20240101", adjust="qfq" ) # 统一字段名,方便后续合并 price_df = price_df.rename(columns={"日期": "date", "收盘": "close", "成交量": "volume"}) price_df["code"] = code price_dict[code] = price_df[["date", "code", "close", "volume"]]

这段代码跑完,你手上是 30 只票各自五年的日线数据。symbol传的是股票代码,period="daily"是日线级别,adjust="qfq"保证复权干净。我一般会先用一个市值适中的股票池把流程跑通,再加到全部成分股。30 只票的数据量已经够你调试特征和模型,全量拉取只是时间问题。如果你有 tushare pro 的 token,也可以换成pro.daily()接口,字段更规范,但需要积分权限,免费方案里 AkShare 足够起步。

2.2 特征工程:动量、波动率与量价因子怎么落到 DataFrame 上

模型差,九成是数据没喂明白。特征工程这一步,源码里给了一套可以直接抄作业的方案:价格动量、收益率、波动率、成交量相对变化,外加一个 RSI 指标。这些特征每一列都有明确的交易含义,不是随便拉一堆数值塞给模型。动量特征代表过去一段时间的价格趋势,波动率特征代表风险敞口,量比特征代表资金活跃度的变化。机器学习算法不会替你理解字段的业务语义,所以每一列都要有说法。

import pandas as pd import numpy as np def build_features(df): df = df.sort_values("date").copy() df["ret_1"] = df["close"].pct_change(1) # 日收益率 df["ret_5"] = df["close"].pct_change(5) # 5日动量 df["ret_20"] = df["close"].pct_change(20) # 20日动量 df["vol_20"] = df["close"].rolling(20).std() # 20日波动率 df["volume_ratio"] = df["volume"] / df["volume"].rolling(20).mean() # 量比 df["rsi"] = 100 - 100 / (1 + df["ret_1"].rolling(14).mean().clip(lower=0) / (-df["ret_1"].clip(upper=0)).rolling(14).mean().replace(0, np.nan)) return df.dropna()

这段函数返回的 DataFrame 里,ret_1到rsi就是后续喂给模型的全部特征。pct_change(n)计算的是 n 日前的收盘价到当前收盘价的涨跌幅,rolling(20).std()取的是最近 20 天的标准差。RSI 那行公式看着绕,本质是过去 14 天上涨均值除以下跌均值再映射到 0 到 100 区间。这里要注意dropna():前 20 行因为 rolling 窗口不够会被丢掉,这是正常现象,不是 bug。特征不是越多越好,ret_1、ret_5、ret_20之间存在天然相关性,树模型对这种冗余不敏感,但如果你后续换线性模型或神经网络,要提前做相关性过滤。

2.3 标签定义与时间序列切分:预测未来几天上涨

特征准备好之后,接下来要回答的问题是“模型到底学什么”。这是量化研究里最容易想当然、也最容易埋雷的一步。源码里使用的是分类任务:未来第 5 个交易日的收盘价是否高于当前收盘价,高于则标签为 1,否则为 0。把它定义成二分类而不是直接回归预测收益率,原因是回归任务对离群值太敏感,一次极端涨跌就能把损失函数带偏,而分类标签更稳健。

def build_label(df, horizon=5): df = df.sort_values("date").copy() df["future_close"] = df["close"].shift(-horizon) # 取未来第5天收盘价 df["label"] = (df["future_close"] > df["close"]).astype(int) return df.dropna(subset=["label"])

shift(-horizon)是整份源码里最值得盯住的一行。它把未来第 5 天的收盘价搬到当前行,与当前收盘价比较得到标签。这个操作本身不引入未来函数,因为特征仍然只使用截至当前时刻的数据,标签在训练时被清洗成监督信号。但如果你后续做特征时用了未 shift 的 rolling 均值,或者归一化时跨了样本,未来信息就会漏进去。后面避坑章我会专门展开,这里只需要记住:特征只能用当前及过去的数据,标签只能指向未来。这是量化机器学习不可动摇的红线。

3. 模型选型与训练:从随机森林到 XGBoost 怎么选

3.1 机器学习和传统技术指标的本质区别

为什么不用 RSI、MACD 这类传统规则直接做信号?因为规则是静态的,阈值写死了,行情风格一变就被市场收割。机器学习的做法是把 RSI、均线、动量这些因子作为特征喂给模型,让模型自己学出“当下这些指标处于什么组合时,未来上涨概率更高”。这也是源码的价值所在:它把因子从“指标公式”提升为“模型输入”。你在很多平台看到的三步点金、量能饱和度这类指标公式,本质是人为设定阈值;而随机森林和 XGBoost 是在数据里找非线性组合,泛化边界完全不同。

3.2 四个候选模型的取舍对比

源码里出现的模型,按我的实测经验可以分成两档。决策树和随机森林适合快速基线,XGBoost 适合追求收益上限,LSTM 在这份源码里是可选模块,对数据量和调参要求都更高。我建议你第一次跑通时用 XGBoost,它的训练速度和默认参数表现都相对稳定。

模型优势劣势适合场景
决策树可解释性强单棵树过拟合严重理解特征分裂逻辑
随机森林方差小,抗过拟合对噪声敏感度一般快速验证特征有效性
XGBoost带正则化,精度上限高超参数多,调参成本高主力模型
LSTM能建模序列依赖数据量要求高,易过拟合长周期时序研究

3.3 训练流程:TimeSeriesSplit 是时序模型唯一的切分方式

训练过程中有个细节比模型本身更重要:数据切分。千万不要用train_test_split随机切分,它会把 2022 年的数据塞进训练集、2020 年的数据塞进验证集,模型学到了未来的行情规律,回测自然漂亮,实盘必翻车。时序问题必须按时间顺序切分,sklearn 的TimeSeriesSplit就是为此设计的。每一折都用更早的数据训练、更晚的数据验证,模拟的是“用历史预测未来”的真实推演。

from sklearn.model_selection import TimeSeriesSplit import xgboost as xgb X = df[["ret_1", "ret_5", "ret_20", "vol_20", "volume_ratio", "rsi"]] y = df["label"] tscv = TimeSeriesSplit(n_splits=5) for fold, (train_idx, val_idx) in enumerate(tscv.split(X)): X_train, X_val = X.iloc[train_idx], X.iloc[val_idx] y_train, y_val = y.iloc[train_idx], y.iloc[val_idx] model = xgb.XGBClassifier( n_estimators=100, max_depth=3, learning_rate=0.05, subsample=0.8, colsample_bytree=0.8, reg_lambda=1.0, eval_metric="auc" ) model.fit(X_train, y_train) print(f"fold {fold}, AUC: {model.score(X_val, y_val)}")

n_estimators=100是树的数量,max_depth=3限制单棵树深度,都是控制过拟合的关键参数。learning_rate=0.05是学习率,调低可以让训练更稳但更慢。subsample=0.8和colsample_bytree=0.8分别是对样本和特征做随机采样,这两项是 XGBoost 抗过拟合的核心手段。reg_lambda是 L2 正则项,值越大模型越保守。这套参数是量化场景的常用起步值,不一定是源码里默认的,但它比默认参数更不容易在验证集上直接崩掉。

3.4 滚动训练视角:每个 fold 都重新训练

上面循环里每次 fold 都新建了一个模型并从头训练。实际研究中更专业的做法是保留一部分最早的数据作为固定训练集,然后像滚动窗口一样逐步往后推进:每加入新一批数据就重新训练一次,验证紧跟其后的行情区间。源码里没有做这个高级特性,但这就是自己动手可以补强的地方。往下我会在第 5 章具体说滚动训练怎么落地。

4. 回测与避坑:五个把结果毁掉的隐性错误

4.1 回测框架的最低要求:能算长期收益曲线

回测是量化研究里最考验细节的环节。源码里给的是向量化回测方式:用 pandas 对整条收益序列做向量运算,比事件驱动的回测框架快,适合验证日线级别的策略。需要手动处理手续费和滑点,这两项是回测失真最大的来源。

def run_backtest(df, signal_col="pred_label", fee=0.0003, slippage=0.0002): df = df.sort_values("date").copy() df["position"] = df[signal_col].shift(1) # 次日开盘才执行,避免未来函数 df["ret"] = df["close"].pct_change(1) df["strategy_ret"] = df["position"] * df["ret"] - fee - slippage df["cum_return"] = (1 + df["strategy_ret"]).cumprod() return df

这里的核心是position.shift(1)。模型在第 t 天收盘后给出预测信号,实际下单发生在第 t+1 天开盘。如果直接用当天的信号乘当天收益,就相当于你预言了当天收盘价,回测曲线会好到不真实。fee=0.0003是单边万三手续费,slippage=0.0002是万分之二的滑点,A 股双边加起来大约是千分之一。别小看这千分之一,高频调仓的策略一年换手几十次,成本会被它吃掉一大块。

4.2 参数调优的边界:网格搜索不是万能后悔药

源码里给的模型参数算保守,但如果你做网格搜索遍历上千组参数,回测曲线很可能会异常漂亮。这不是因为找到了圣杯,而是因为你搜索了太多组参数,总有一组恰好贴合历史行情的噪声。我的做法是先把参数范围缩小,用随机搜索代替网格搜索,并且每组参数都用 TimeSeriesSplit 的完整流程验证,不要让单一 fold 的 AUC 决定去留。

4.3 五个高频坑:现象、原因、解决

坑 1:回测年化 50%,实盘一个月就亏钱

现象:回测曲线平滑上行,模拟盘连续亏损,而且亏损方式与回测中的回撤完全对不上。

原因:特征或标签里混入了未来信息,最常见的写法是close.rolling(5).mean()后没有 shift,计算均值时包含了当天收盘之后的未来数据,但模型在当天收盘前是不可能知道这个值的。另一种常见情况是把整个数据集的均值和标准差算好后再归一化,导致验证集的信息提前进入了训练集。

解决:所有特征列在生成时只允许使用截至当前行的数据;需要归一化的特征放在滚动窗口内做,比如close.rolling(20).mean()本身已经只包含历史数据;归一化参数只能在训练集上 fit,验证集只做 transform。养成一个习惯:每生成一列特征,就自问一句“这一列在第 t 天下午收盘时真的已经知道了吗?”

坑 2:训练集 AUC 0.85,验证集 AUC 只有 0.52

现象:训练指标很好,一换到验证集立刻崩盘,跟随机猜测差不多。

原因:这是标签泄漏的典型表现。最常见的是shift(-horizon)没有对齐,或者标签构建时不小心把当天的收益率也参与了标签计算。也会发生在 pct_change 的边界值上:第一天的pct_change是 NaN 被 drop 掉了,但 drop 之前标签已经错位。

解决:把特征和标签拆成两个函数分别构建,每个函数都单独测试。验证标签对齐的方式是打印出df[["date", "close", "future_close", "label"]],人工看几行,确认“标签 1 意味着未来第五天真的涨了”。这一步三分钟,能省掉后面三天排查时间。

坑 3:回测稳定盈利,换一个时间段就彻底失效

现象:同样一套源码,回测 2020–2022 稳稳赚钱,换成 2016–2019 就亏到不能看。

原因:模型过拟合了特定行情风格。比如你在 2020–2022 这段结构性行情里训练,模型学到的是白马股抱团特征,换到 2016 年小票行情自然失效。另一种可能是网格搜索时每一组参数都在数据上验证过,最优参数只是历史巧合。

解决:用 walk-forward 验证代替单次回测。把完整时间序列切成多个连续的训练/验证对,比如 2016–2018 训练、2019 验证;2017–2019 训练、2020 验证;2018–2020 训练、2021 验证。每个验证段都不参与训练,最终看所有验证段的综合表现。这是行业里公认最接近实盘推演的评价方式。

坑 4:回测选出的股票池里全是“活到现在的票”

现象:回测收益不错,但仔细一看,历史信号里买入的股票现在已经退市了。

原因:幸存者偏差。如果你用今天沪深 300 的成分股名单去回测五年前的信号,就等于排除了当时在指数里、后来被剔除或退市的票。机器学习模型只会学到“这类公司活下来了”的规律,实盘时买到的未经历筛选的股票,模型并未见过。

解决:回测时必须使用“当时可获得的成分股名单”。AkShare 可以按季度拉取历史成分股快照,或者退一步,用全 A 股数据做训练,而不是以当前成分股为准。至少要在回测报告里标注清楚股票池口径,不能默认“现在活的代表过去也活”。

坑 5:信号在回测里成交价完美,实盘一买就冲高

现象:回测里模型给出买入信号后,当天都以收盘价成交;实盘同样操作用的是次日开盘价,价格高了几个点。

原因:小市值或流动性差的股票,次日开盘价往往相对前日收盘价有跳空。回测中没有建模这种价格冲击,策略里也没有过滤流动性差的标的。

解决:两个措施同时做。第一,回测默认用次日开盘价成交而不是当天收盘价,至少偏离实际情况更近;第二,在选股阶段加一个成交量过滤条件,比如过去 20 日平均成交额低于 2000 万的股票直接排除。不要跟流动性较劲,你的资金体量越大,这个坑越深。

5. 从回测到实盘:用模拟盘验证和滚动训练收尾

回测通过只是开始。源码包附带项目说明文档,这一点很关键——它给出了这套算法在真实场景里的部署建议。当年我自己第一版策略死在“回测三年翻五倍”的幻觉里,实数盘三个月就撤了。后来我把源码里的流程改造成一套更稳的工作方式,核心只有一个:模拟盘先行,滚动训练常驻。

具体做法是先用回测代码输出每日持仓信号,手动或通过券商的模拟盘 API 下单,坚持跑八周以上。这期间重点记录三件事:信号与次日实际涨跌幅的偏差、策略换手率与回测的差异、最大回撤发生的日期。为什么要八周?因为 A 股一个月左右的行情风格切换已经能暴露很多回测看不出的问题。八周模拟盘如果收益曲线形态与回测基本一致,再考虑小仓位实盘。

滚动训练这一步,我把它固化成了一个习惯:每周一收盘后,重新拉数据、重新构建特征、重新训练模型,验证集固定为最近三个月,模型只预测未来五个交易日。这样做的好处是模型始终在适应当前行情,而不是拿着半年前的参数硬扛。源码里 TimeSeriesSplit 的循环结构可以直接改造成月度滚动,把 5 折循环换成for month in date_range:,每轮把截止到当月的全部数据作为训练集,预测下一个月。你并不需要一套新代码,需要的是把训练逻辑从“一次性”改成“周期性”。

从那以后,我每拿到一份量化方向的源码,第一件事不再是跑回测曲线有多漂亮,而是打开特征工程文件检查有没有未来函数,再打开标签定义检查 shift 的方向。这两个位置干净了,后面的模型和回测才有意义。这份源码的价值就在这里:它给了你一个完整的、可以逐行检查的起点,剩下的路要靠你自己一步步验证走下去。希望帮到你。

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

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

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

立即咨询