机器学习股票预测源码解析:从特征工程到回测验证的完整指南
简介针对股票市场股价分析与预测的机器学习项目源码包面向计算机、人工智能、大数据、数学等专业正在开展课程设计、期末大作业或毕业设计的学生也适合有一定Python基础的算法爱好者作为实践参考。资源共2个文件包含Python算法主程序与配套的说明/依赖文件压缩包仅45KB体积小巧、结构清晰代码经过严格调试下载后即可直接运行验证。目前已有298人参与学习下载使用口碑较好。通过该源码读者可以完整看到从历史行情数据读取、特征工程处理、模型构建训练到股价趋势预测输出的全流程理解机器学习在金融量化场景中的落地思路配套说明文档也对代码模块和参数作了必要注释有助于快速上手修改和二次开发。整体项目既可作为课程设计或毕设的完整交付物也适合作为算法对比实验的基线代码对提升数据分析与建模能力颇有帮助无论用于课程汇报还是个人学习都能获得一套可复用的项目模板。1. 拿到股票预测源码包之前先想清楚它到底是什么说实话打开任何“基于机器学习算法的股票价格分析及预测源码”压缩包最危险的一刻就是看到回测曲线斜向上、年化收益几十个百分点的那一眼。机器学习预测股票这件事方向没有传说中那么玄但坑比大部分人想象中多得多。这个标题把三件事绑在了一起机器学习算法、股票价格的分析及预测、可运行的源码。它意味着你要面对的不仅仅是训练一个模型而是一条从数据清洗、特征构造、模型选型到回测验证的完整流水线。这篇笔记写给两类人拿源码做毕设或课程设计的开发者以及想评估这类项目能不能落到实盘交易的量化新手。我会把这条流水线拆开讲清楚每一步怎么写、参数怎么调、哪几个地方最容易翻车。2. 数据与特征工程先解决“模型吃什么”再谈算法选型2.1 模型实际上在吃什么OHLCV与三类特征来源股票预测源码打开之后第一步一定是处理行情数据。最常见的行情字段就是 OHLCVopen开盘价、high最高价、low最低价、close收盘价、volume成交量有的数据源还会带上 amount成交额和 preclose昨收盘。模型要吃的是这些原始量价数据以及它们衍生出来的特征。我一般把特征来源分成三类原始量价、技术指标的数学变换、时间窗口统计量。技术指标这块最常出现的是 MA、RSI、MACD、布林带这些但它们之间相关性很高堆太多指标进去模型学到的其实是同一个信息的多次变换。时间窗口统计量则包括滚动均值、滚动标准差、滚动分位数比如过去 5 天收益率均值、过去 20 天波动率这些能告诉模型当前处于什么市场状态。股价预测本质上是从历史形态外推特征工程要解决的是让模型看到“形态”而不是看到绝对价格。绝对价格的数值大小没有意义因为不同股票股价区间差异巨大所以后面一定要做归一化。2.2 用 baostock 拉日线数据一个能落地的数据接入脚本很多源码包的瓶颈不在模型在数据接口。我常用的数据源是 baostock免费、国内可访问、返回格式稳定适合复现和教学。拉取日线数据的脚本骨架如下import baostock as bs import pandas as pd # 登录数据服务 lg bs.login() # 获取股票日K线这里以 sh.600519 为例 rs bs.query_history_k_data_plus( sh.600519, date,code,open,high,low,close,preclose,volume,amount,pctChg, start_date2018-01-01, end_date2023-12-31, frequencyd, adjustflag2 # 1后复权 2前复权 3不复权 ) # 遍历结果集拼成 DataFrame data_list [] while (rs.error_code 0) and rs.next(): data_list.append(rs.get_row_data()) df pd.DataFrame(data_list, columnsrs.fields) bs.logout()代码里的参数值得说一下。frequencyd 表示日线改成 w 就是周线m 是月线做中短期预测一般用日线。adjustflag 决定复权方式前复权适合看历史图形走势但不适合做回测原因后面在避坑章节会展开如果你要的是真实成交价用 adjustflag3 取不复权再配合复权因子自行处理。登录和登出是 baostock 的标准动作如果 login 返回失败通常是被数据源的连接数限制了等一会儿再试即可。2.3 特征衍生与归一化注意窗口边界别让未来数据偷跑拿到原始日线后接下来做特征衍生。我会算收益率、均线、波动率、量比和 RSI同时构造预测标签。核心代码如下import numpy as np # 基础收益率特征 df[return_1d] df[close].pct_change() df[ret_5d] df[close].pct_change(5) df[ma_5] df[close].rolling(5).mean() df[ma_20] df[close].rolling(20).mean() df[vol_20] df[return_1d].rolling(20).std() df[volume_ratio] df[volume] / df[volume].rolling(20).mean() # RSI 指标 delta df[close].diff() gain delta.clip(lower0).rolling(14).mean() loss (-delta.clip(upper0)).rolling(14).mean() df[rsi_14] 100 - 100 / (1 gain / loss) # 构造预测标签次日收盘价是否高于今日收盘价 df[label] (df[close].shift(-1) df[close]).astype(int) # 删除因窗口计算产生的 NaN 行 df df.dropna().reset_index(dropTrue)窗口参数 5、20、14 是技术分析里的常用经验值分别对应短期均线、月均线和 RSI 默认周期。你可以先固定这一组后续用滚动窗口做敏感性分析再微调。注意 shift(-1) 的用法标签必须严格指向未来如果用错了 shift 方向模型就会用当天数据去预测当天涨跌等于直接作弊。特征衍生完成后还要对特征做归一化常见做法是标准化from sklearn.preprocessing import StandardScaler feature_cols [return_1d, ret_5d, ma_5, ma_20, vol_20, volume_ratio, rsi_14] scaler StandardScaler() # 只对特征归一化标签保持 0/1 df[feature_cols] scaler.fit_transform(df[feature_cols])注意这里的坑在于 scaler 必须在训练集上 fit测试集只能 transform。如果你把全量数据一起 fit标准化时就会用到测试集的均值和方差等于让模型偷看了未来的分布回测结果虚高。这一点在源码包里出现频率极高。3. 机器学习算法选型与训练让模型学会看涨跌而不是背行情3.1 先定义任务预测涨跌方向比预测具体价格更稳拿到源码包后第一个要确认的事情是任务类型的定义。常见做法有两种回归任务预测明天的收盘价具体数值分类任务预测明天股价是涨还是跌。我的建议是优先做分类。原因很实在股票价格近似随机游走回归模型学到的最优解往往是把昨天的收盘价当成今天的预测值误差很小但对交易没有任何指导意义。而分类任务直接对齐交易动作——预测涨就买入预测跌就空仓评估起来也直观。A股是单边做多为主的市场预测“跌”的实际动作是不持仓而不是做空所以二分类涨/跌足够覆盖大部分交易策略。如果打算精调可以把任务扩展成三分类显著上涨、横盘、显著下跌但样本不均衡的问题会更严重。很多源码包默认做二分类就是这个原因。3.2 从常用机器学习算法里挑出适合股价预测的那几个在机器学习算法这个大类下适合股票结构化数据的模型其实就那几种。我做个对比方便你拿到源码包后快速判断它用的算法靠不靠谱模型定位最大优势最容易翻车的地方LASSO / 线性回归基线模型解释性好能看到特征方向线性假设太强抓不住非线性形态随机森林集成基线对异常值鲁棒不容易过拟合无法外推训练区间之外的数值关系XGBoost / LightGBM主力模型处理结构化特征稳定调参空间大参数多max_depth 拉高后过拟合很快LSTM / GRU序列模型理论上能捕捉长周期依赖数据量不够时远不如 XGBoost 稳定很多源码包会把 LSTM 也归到“机器学习算法”里一起混着卖。LSTM 不是不能用而是它对数据量和训练稳定性要求高日线数据一年只有 250 根左右五年也就 1200 根这点样本量很难让循环神经网络发挥优势。我一般建议以 XGBoost 或 LightGBM 作为主力模型跑通之后再考虑要不要上深度模型。3.3 训练脚本的最小可运行骨架以 XGBoost 为例下面是一段可以直接落地的训练脚本配合前文的特征数据使用。它做了时间序列切分、模型训练、早停和验证集准确率输出import xgboost as xgb from sklearn.model_selection import TimeSeriesSplit from sklearn.metrics import accuracy_score feature_cols [return_1d, ret_5d, ma_5, ma_20, vol_20, volume_ratio, rsi_14] X df[feature_cols] y df[label] # 时间序列切分顺序保留不打乱样本 tscv TimeSeriesSplit(n_splits5) params { objective: binary:logistic, eval_metric: logloss, max_depth: 4, learning_rate: 0.03, subsample: 0.8, colsample_bytree: 0.8, seed: 42 } 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] dtrain xgb.DMatrix(X_train, labely_train) dval xgb.DMatrix(X_val, labely_val) model xgb.train( params, dtrain, num_boost_round1000, evals[(dval, val)], early_stopping_rounds50, verbose_evalFalse ) pred (model.predict(dval) 0.5).astype(int) print(fFold {fold 1} val accuracy: {accuracy_score(y_val, pred):.3f})重点说明几个参数。max_depth4 是经验值树的深度拉到 5 以上在股票这种信噪比很低的数据上过拟合非常快。learning_rate0.03 配 early_stopping_rounds50意思是验证集 logloss 连续 50 轮不下降就停这是防止过拟合的主要手段。subsample 和 colsample_bytree 都设 0.8让每棵树只用 80% 的样本和 80% 的特征增加多样性。seed42 必须固定否则同一份代码每次跑出来的结果都不一样后面的分析与调参就无从谈起。3.4 用 TimeSeriesSplit 替代普通 KFold防止未来函数的第一道闸门股票数据不是独立同分布的样本它有时间顺序。普通 KFold 交叉验证会把样本随机打乱这等于让模型用 2022 年的数据去训练再把 2021 年的数据放进训练集最后拿 2023 年的数据做验证。前文代码里的 TimeSeriesSplit 是专门为时间序列设计的切分方式训练集永远在验证集之前不会出现“用未来预测过去”的作弊行为。这是整个预测流程里最基础也最重要的一道闸门。很多人看到源码包里的准确率很高第一反应是模型厉害但更常见的原因是切分方式本身就有问题。你可以快速验证一下把源码里的 train_test_split 或 KFold 替换成 TimeSeriesSplit准确率通常会明显下降。下降不可怕下降之后的数字才是模型真实水平的近似估计。如果替换后模型准确率仍然超过 60%这个模型才有进一步优化的价值。4. 回测验证用一套可复现的脚本看清策略真实水平4.1 回测脚本的最小骨架先解决“预测结果怎么变成钱”模型输出的预测只是 0 和 1真正要评估它有没有用必须把预测结果翻译成资金曲线。很多源码包在这里偷懒只用准确率交差这是不够的。一个最小可用的回测脚本应该包含买入、卖出、手续费和持仓状态的记录。下面是我常用的骨架def backtest(df, signal_colpred, initial_cash100000, fee_rate0.001): df df.copy() # 今天持仓取决于昨天的信号避免未来函数 df[position] df[signal_col].shift(1).fillna(0).astype(int) cash initial_cash holding 0 entry_price 0.0 equity_curve [] for idx, row in df.iterrows(): # 开仓当天开盘买入 if row[position] 1 and holding 0: entry_price row[open] holding cash * (1 - fee_rate) / entry_price cash 0.0 # 平仓当天开盘卖出 elif row[position] 0 and holding 0: cash holding * row[open] * (1 - fee_rate) holding 0.0 total_asset cash holding * row[close] equity_curve.append(total_asset) df[equity] equity_curve df[strategy_ret] df[equity].pct_change().fillna(0) return df这里有一个关键设计以昨日信号为今日持仓依据成交价全部用当日开盘价。为什么不用当日收盘价因为预测结果是用昨日收盘后的数据算出来的逻辑上只能在今日开盘时成交。如果源码包里的回测用当日收盘价成交说明它把“预测当日涨跌”和“当日收盘买入”混在了一起白白占了一个 K 线级别的便宜。fee_rate 我设为 0.001粗略估算单边佣金加印花税反复调仓时这一点差别很大。4.2 三个必看数字年化收益、最大回撤、夏普比率回测跑完不要只看资金曲线长什么样要算三个数字年化收益率、最大回撤、夏普比率。它们分别回答三个问题这个策略赚不赚钱、扛不扛跌、风险调整后值不值得做。计算逻辑如下days len(df) total_ret df[strategy_ret].add(1).prod() - 1 annual_ret (1 total_ret) ** (days / 252) - 1 cum_max df[equity].cummax() drawdown df[equity] / cum_max - 1 max_drawdown drawdown.min() sharpe df[strategy_ret].mean() / df[strategy_ret].std() * np.sqrt(252) print(f年化收益: {annual_ret:.2%}) print(f最大回撤: {max_drawdown:.2%}) print(f夏普比率: {sharpe:.2f})这三个数字的判断标准可以参考下表指标计算方式可接受的下限年化收益率复利折算到一年至少要跑赢国债收益率最大回撤资金曲线从峰值回落的最大幅度超过 20% 需要谨慎夏普比率超额收益除以波动率再年化大于 1 说明风险调整后收益为正4.3 永远要和基准比跑不赢指数的策略不如直接买 ETF单独看年化收益是片面的。2019 年到 2021 年的结构性行情里很多股票本身涨幅巨大一个简单的买入持有策略都能有不错的收益曲线。所以必须引入基准对比通常用沪深 300 指数或中证 500 指数df[bench_ret] df[bench_close].pct_change().fillna(0) bench_total df[bench_ret].add(1).prod() - 1 excess_total total_ret - bench_total print(f策略总收益: {total_ret:.2%}) print(f基准总收益: {bench_total:.2%}) print(f超额收益: {excess_total:.2%})这一步能过滤掉大量伪策略。如果模型预测出来的择时结果还不如一直拿着指数基金那这套机器学习源码的真正价值就要打个问号了至少不能直接用。反之如果超额收益稳定为正那说明模型确实从数据里捕捉到了一些规律。5. 排查与避坑五个让回测曲线“翻车”的隐蔽原因5.1 训练集准确率很高实盘却连续亏损这是所有股票预测源码包里最常见的问题。现象是模型在训练集上准确率高达 90% 以上回测曲线也很好看但换到实盘或新数据上就连续止损。原因是典型的过拟合加未来函数特征标准化时用了全量数据或者标签构造时 shift 方向写错导致模型实际上看到了未来的信息。解决方法是严格按时间顺序切分数据标准化只用训练集 fit标签必须用 shift(-1) 构造并检查代码里有没有非滞后特征混入。做完这三步准确率通常会从 90% 掉到 55% 左右这才是模型真实水平的起点。5.2 换个股票代码模型就完全失效现象是在某只股票上测出来的结果不错一旦换代码预测准确率直接跌到 50% 附近。原因是单个股票上训练出来的模型参数严重过拟合到该股票的波动特点上。A股个股之间的走势节奏差异很大有的股波动剧烈有的股长期横盘单一标的的样本很难覆盖这些情况。解决思路是构建一个由多只不同行业股票组成的数据池按行业均衡抽样来训练让模型学习的是共性的技术形态规律而不是某只股票的脾气。评估模型时也应该用“训练集股票池之外”的股票做验证这一步能过滤掉大量虚假的选股能力。5.3 同一份源码换个机器结果就对不上这是一个容易被忽视但很致命的问题。现象是明明代码没改数据没变但跑出来的准确率、资金曲线每次都不一样。原因是随机种子没有全部固定sklearn 的模型要设 random_stateXGBoost 要设 seed数据切分时还要设置 shuffle 的随机状态。如果你在源码里找不到这些参数赶紧自己加上。模型结果对不上意味着你所有结论都无法复现调参也变成玄学。解决方法是把前文代码里的 seed42 贯彻到所有带随机性的环节。5.4 预测输出全部是同一个值模型像“死机”了一样现象是回测日志里模型预测的概率一直徘徊在 0.5 附近或者全部输出为 0。原因除了标签构造错位还有一个常见的来源特征矩阵里有过多的缺失值虽然 XGBoost 能自动处理缺失但当特征里 NaN 占比过高时模型学到的分叉规则会变得非常病态。解决方法是先在特征衍生后执行 dropna再看标签分布是否均衡。如果标签里上涨样本占比只有 20%模型为了逞准确率就会全部预测为 0这时候需要调整预测阈值或加 class_weight而不是直接使用默认的 0.5 阈值。5.5 回测中出现了“未来才知道”的复权价格这一条是进阶玩家才容易踩的坑。现象是某段历史回测收益极高仔细看成交记录发现买入价总是恰好接近最低点。原因很可能是使用了前复权价格做回测。前复权价格会随着每次除权除息重新计算历史 K 线也就是说你用来调参的历史价格序列可能已经包含了未来除权事件的信息。解决方法是训练和回测时用不复权的价格或者用后复权价格。后复权的历史价格是固定不变的不会因为新的分红事件而整体平移更适合作为回测数据源。这个问题在源码包里几乎从不标注需要自己留意。6. 从能跑到敢用滚动训练与信号过滤的小技巧模型跑通只是起点能不能接近可用取决于你有没有解决“模型过期”的问题。市场风格会漂移两年前有效的特征关系今年可能完全反转。我的习惯是加一层滚动训练机制用最近 252 个交易日约一年的数据训练每 63 个交易日约一个季度重新训练一次让模型持续吸收最新行情特征。代码骨架很简单核心是让训练窗口不断向前滑动而不是只训练一次就永久使用。信号过滤是另一个投入产出比很高的技巧。模型每天都会输出一个预测但高频交易在A股会受到手续费和滑点的双重磨损。我一般会要求预测信号连续三天保持一致才触发买入比如连续三天预测上涨才建仓连续三天预测下跌才清仓。这个规则会把交易次数降到原来的三分之一资金曲线会平滑不少夏普比率往往不降反升。评判这类策略的真正标准只有一个超额收益是否稳定为正。不要追求准确率准确率 55% 但只在信号强烈时出手的策略远比准确率 70% 但频繁交易来得好。我自己在调参上吃过不少亏最后养成的习惯是每次只改一个参数固定同一段回测区间所有实验记录在案。哪怕是 XGBoost 里一个小小的 min_child_weight改完也要跑一遍完整回测对比。源码包里那些效果爆表的参数组合很多都是在同一段数据上来回试出来的换一段行情就失效真正能在实盘中用得住的永远是经得起时间切分、基准对比和滚动验证的策略。希望帮到你。本文还有配套的精品资源点击获取