Python机器学习股票预测:从特征工程到回测实战

发布时间:2026/9/20 10:01:05
Python机器学习股票预测:从特征工程到回测实战 简介在量化投资领域股票预测是机器学习技术最典型的应用方向之一。其核心并非追求模型复杂度而是正确处理时间序列数据、避免数据泄露并通过特征工程将原始行情转化为可学习的量价关系。基于Python生态利用pandas处理OHLCV数据构建技术指标如RSI、MACD、布林带等再借助LightGBM等梯度提升树模型进行有监督学习预测未来涨跌方向。为了评估策略有效性还需构建回测框架计算最大回撤、夏普比率等指标。这一套流程从特征到模型再到回测构成了量化交易系统的基础。文章围绕股票预测项目详细拆解了从数据处理、特征构建到模型训练与参数调优的全过程并重点剖析了常见的数据泄漏问题与解决方案为毕业设计或量化入门提供完整参考。1. 股票预测项目没你想的那么玄但别指望它赚钱用Python做股票预测是机器学习入门里最容易“做出来”的方向也是最容易在答辩时被问住的方向。这里的原因在于股价预测的难点永远不在模型而在数据对齐和未来函数。很多毕设代码跑出来的准确率高达九成以上其实只是把未来数据泄露进了训练集。本文要拆的这套“基于机器学习的股票预测和分析”核心工作是把量价数据变成特征矩阵用有监督学习拟合次日或未来N日的涨跌方向最后配合回测来评估策略有效性——而不是直接输出“明天涨还是跌”这种无法验证的结论。适合准备毕业设计、想系统过一遍金融ML流程或者刚入行想搞清楚数据泄露的人。2. Python股票预测的项目结构与技术选型2.1 这类型项目的典型文件组织方式不管是自己从零写还是拿到了他人源码一个合格的股票预测毕设项目通常长这样stock_predictor/ ├── data/ # 原始行情与中间缓存 │ ├── raw/ # 从数据源拉取的CSV │ └── processed/ # 特征工程后的parquet ├── src/ │ ├── data_fetcher.py # 数据获取 │ ├── features.py # 技术指标与特征构建 │ ├── model_trainer.py # 模型训练与调参 │ ├── backtest.py # 回测引擎 │ └── utils.py # 通用函数 ├── notebooks/ # EDA与实验记录 ├── docs/ # 毕业设计文档 └── config.yaml # 全局配置这种组织的逻辑是数据获取、特征工程、模型训练、回测评估四段解耦。它直接对应论文里“数据来源—特征设计—模型构建—实验分析”的章节结构也让答辩时被问“某一块怎么改”时能快速定位代码位置。2.2 技术栈怎么选才合理毕设场景下技术选型的核心标准不是“最先进”而是“可解释且能跑通”。以下组合是最常见也最稳的模块推荐库选择理由数据获取yfinance / akshareyfinance免费无需tokenA股用akshare数据处理pandas / numpy行业标准答辩必问特征工程pandas / ta自己手写指标更能讲清原理模型LightGBM / XGBoost / sklearn集成表格数据上梯度提升树综合表现优于深度学习且训练快深度学习备选PyTorch LSTM论文需要“创新点”时用可视化matplotlib / pyecharts画K线和技术指标回测backtrader / 手写手写更透明毕设优先手写注意图上没有把神经网络作为主力模型。原因很简单单支股票的日线数据通常只有几千条LSTM在这种数据量下很难比梯度提升树更有优势还容易在答辩时被问“为什么用LSTM不用GRU”这种纠缠不清的问题。2.3 数据源与label定义是项目的生死线数据获取阶段的第一个坑是复权。股票分红、送股会导致价格跳空不处理复权的话模型会学到“除权日必跌”这种失真规律。推荐用前复权数据因为前复权保留了当前最新价格更适合分析历史走势。label定义决定了模型学什么。常见三种# 二分类次日涨跌 df[label_cls] (df[close].shift(-1) df[close]).astype(int) # 三分类次日涨/跌/平 diff df[close].shift(-1) - df[close] pct diff / df[close] df[label_3cls] np.where(pct 0.005, 1, np.where(pct -0.005, 2, 0)) # 回归未来5日收益率 df[label_reg] df[close].shift(-5) / df[close] - 1这段代码里有三个关键细节。shift(-1)是从t时刻看t1的收益方向确保预测时只用收盘后已知数据。三分类的0.005阈值可调作用是过滤噪音波动。label_reg用未来5日收益率对应“预测未来一周趋势”这个更实际的需求。真正容易出错的地方在于做特征时千万不能用未来数据。比如用当日最高价做特征如果目标是预测次日方向当日最高价本身包含当日信息、没问题但当日最高价与次日label之间没有未来泄漏。3. 特征工程从裸行情到机器学习能用的矩阵3.1 基础特征从哪来原始行情只有OHLCV开高低收量直接丢给模型什么也学不到。需要把量价关系转化为技术指标。这里的关键原则是特征要覆盖三类信息价动量、均值回归、量资金参与度、量价结合供需变化。窗口类特征是最基础的一层def add_window_features(df, windows[5, 10, 20, 30]): for w in windows: # 动量当前价相对w天前的涨幅 df[fret_{w}] df[close] / df[close].shift(w) - 1 # 均值偏离当前价相对w日均线的偏离度 df[fma_dev_{w}] df[close] / df[close].rolling(w).mean() - 1 # 波动率收益率的标准差衡量风险 df[fvolatility_{w}] df[close].pct_change().rolling(w).std() # 量能变化当日成交量相对w日均量的倍数 df[fvolume_ratio_{w}] df[volume] / df[volume].rolling(w).mean() return dfret表示短中期动量正ret暗示趋势向上ma_dev描述价格对均线的乖离值过大时向均值回归的压力增加volatility在金融数据里呈现聚集性大波动后往往跟大波动volume_ratio达到2以上意味着放量是资金异动信号。这两年量化语境下这类量价特征常被称为“量能饱和度”或“量能比”本质都是描述量价关系。3.2 行业通用的经典指标类特征技术指标不是越多越好而是每个都有明确业务含义。最常用的就是均线类指标 MACD、布林带、RSI。RSI指标专门用来刻画“超买超卖”def add_indicators(df, rsi_period14): # RSI相对强弱指数 delta df[close].diff() gain delta.clip(lower0).rolling(rsi_period).mean() loss (-delta.clip(upper0)).rolling(rsi_period).mean() rs gain / loss df[rsi] 100 - 100 / (1 rs) # 布林带位置当前价格在通道中的相对位置 mid df[close].rolling(20).mean() std df[close].rolling(20).std() df[bb_position] (df[close] - mid) / (2 * std) # MACD柱面积 ema12 df[close].ewm(span12).mean() ema26 df[close].ewm(span26).mean() df[macd_hist] (ema12 - ema26).rolling(9).mean() - (ema12 - ema26) return dfRSI高于70为超买低于30为超卖但在A股里“超买还能更超买”所以RSI更常用作风险提示特征而非独立交易信号。bb_position把收盘价映射到[-1, 1]区间描述价格处于布林轨道的哪个位置数值接近1时行情情绪偏高可能出现回调。MACD的hist值转正代表多头动能启动连续上升说明趋势加速。3.3 序列特征与交叉验证——最容易写出未来函数的地方时间序列特征的正确做法是滚动窗口计算而不是全局计算。这里有两个原则# 错误示范标准化用了全部数据 from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_scaled scaler.fit_transform(X) # 训练时偷看了未来分布 # 正确做法先切分再fit training portion train_end int(len(X) * 0.8) scaler StandardScaler() X_train_scaled scaler.fit_transform(X[:train_end]) X_test_scaled scaler.transform(X[train_end:])第二个坑是时间和标签不对齐。构建样本时每行特征来自日期t标签来自日期t1或tn这本身没问题。但如果在t1跌了之后你重新把t1的数据也变成特征那t时刻的特征矩阵就包含了t1的收盘信息——这就是数据泄漏。def make_samples(df, feature_cols, label_col, lookback1): lookback1表示用t日预测t1日 samples, labels [], [] data df[feature_cols].values target df[label_col].values for i in range(len(df) - lookback): # 只取i位置之前的信息label是ilookback位置 samples.append(data[i]) labels.append(target[i lookback]) return np.array(samples), np.array(labels)这段代码的核心约束是samples[i]对应data[i]labels[i]对应target[ilookback]中间没有混用。4. LightGBM模型训练与参数调优4.1 LightGBM为什么比随机森林更适合金融时序随机森林这种bagging模型用来跑股票预测也能出结果但有两个问题一是在高维稀疏特征上泛化不如GBDT类模型二是森林中每棵树是独立投票对时间序列的渐变性捕捉能力弱。LightGBM的梯度提升机制能不断“修正”上一轮的残差在表格数据上通常碾压随机森林。毕设场景里用LightGBM还有个优势在于特征重要性分析顺手。model.feature_importances_直接输出每个特征对分裂的贡献度论文里画一张特征重要性的条形图导师会觉得“这学生是理解模型的”。4.2 训练代码的最小可用版本import lightgbm as lgb from sklearn.model_selection import TimeSeriesSplit from sklearn.metrics import accuracy_score, f1_score # 时间序列专用交叉验证 tscv TimeSeriesSplit(n_splits5) # 多轮实验记录最优参数 best_acc 0.0 best_model None params { objective: binary, learning_rate: 0.05, max_depth: 4, num_leaves: 31, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 1, verbose: -1, random_state: 42, } for fold, (train_idx, valid_idx) in enumerate(tscv.split(X)): X_tr, X_val X[train_idx], X[valid_idx] y_tr, y_val y[train_idx], y[valid_idx] train_data lgb.Dataset(X_tr, labely_tr) valid_data lgb.Dataset(X_val, labely_val, referencetrain_data) model lgb.train( params, train_data, num_boost_round300, valid_sets[valid_data], callbacks[lgb.early_stopping(50), lgb.log_evaluation(0)], ) pred (model.predict(X_val) 0.5).astype(int) acc accuracy_score(y_val, pred) print(ffold {fold} accuracy: {acc:.4f}) if acc best_acc: best_acc acc best_model model这里体现了时序交叉验证的精髓TimeSeriesSplit每次都用前面的数据训练、后面的数据验证不会出现“用第100天到第200天的数据预测第50天”的穿越情况。交叉验证结果不是最终指标但第一版实验通常只看这个数值训练集和验证集accuracy差距过大超过10个点就要怀疑泄露或过拟合。4.3 必调的3个参数与其含义毕设答辩高频提问集中在三个参数num_leaves、max_depth、feature_fraction。参数取值范围作用调大后的风险num_leaves16~256控制树模型的复杂度过大直接过拟合训练集无意义逼近100%max_depth3~8限制树的深度配合num_leaves一起约束feature_fraction0.6~0.9每棵树随机采样特征比例过小模型欠拟合但提高鲁棒性在金融数据上特征之间本身有强相关性如5日和10日均线feature_fraction偏小能减少这种共线性对单棵树的影响。lightgbm官方不建议直接调max_depth而是构造 num_leaves与min_data_in_leaf的组合。毕设场景下遵守“浅树多轮小学习率”的组合通常比“深树少轮”更稳。5. 回测与评估让预测结果变得可信5.1 不要只看准确率为什么还要看回测股票预测里准确率超过55%已经算好这个数字放在论文里显得很“弱”但加上回测资金曲线后说服力完全不同——即便准确率只有53%只要盈亏比合理、胜率稳定资金曲线依然可以平滑向上。回测的本质是回答一个问题如果严格按照预测信号的交易操作最终是赚是亏5.2 手写极简回测引擎def run_backtest(df, pred_prob, initial_capital100000, threshold0.55): 策略逻辑预测概率threshold则持有否则空仓 这里为了演示每天全仓买入/卖出不考虑手续费滑点 position 0 # 0空仓 1满仓 capital initial_capital equity_curve [] for i in range(len(df) - 1): # 信号今天用昨天收盘后的模型输出做决策 signal int(pred_prob[i] threshold) today_return df[close].iloc[i 1] / df[close].iloc[i] - 1 # 每天盯盘决策今天决策基于当天收盘后的预测 # 实际交易从次日开盘开始此处简化为次日收益 if position 1: capital * (1 today_return) # 如果今天信号转空执行卖出以今日收盘价成交 if position 1 and signal 0: position 0 # 如果今天信号转多执行买入以今日收盘价成交 if position 0 and signal 1: position 1 equity_curve.append(capital) return equity_curve, capital / initial_capital - 1这段回测代码故意简化了交易成本。在回测报告里务必要注明“未计入手续费和滑点”否则答辩时会被一句“你这策略实盘能跑吗”堵死。实际项目中在买卖逻辑里加一个千分之一的成本扣减是必要的# 示例单边万二佣金千一印花税仅卖出收取 buy_cost 0.0002 sell_cost 0.0002 0.001 if signal 1 and position 0: capital * (1 - buy_cost)5.3 常用的回测指标表格一份合格的回测报告至少需要这些指标指标含义计算公式累计收益率期末/期初-1capital_end / capital_start - 1年化收益率折算到年(1累计收益)^(244/交易日数)-1最大回撤资金曲线峰顶到谷底的跌幅max(peak - trough)/peak夏普比率单位风险超额收益(策略年化收益-无风险利率)/波动率胜率盈利交易次数占比盈利次数 / 总交易次数最大回撤是比收益率更重要的指标。一个回撤40%的策略即使年化80%也大概率在实盘中中途被止损出局。6. 参数敏感性分析与波动率过滤——把策略做成熟的技巧6.1 阈值别拍脑袋做一张参数敏感性矩阵前面回测代码里threshold0.55是拍脑袋定义的。成熟做法是对一组阈值做循环回测看最终收益与最大回撤的变化规律results {} for t in np.arange(0.5, 0.75, 0.05): equity, ret run_backtest(df, pred_prob, thresholdt) mdd max_drawdown(equity) results[round(t, 2)] {收益: ret, 最大回撤: mdd} for threshold, metrics in results.items(): print(fthreshold{threshold:.2f}, 收益{metrics[收益]:.2%}, 最大回撤{metrics[最大回撤]:.2%})输出结果是连续变化的提高阈值往往意味着交易次数变少、单笔更确定但同时会漏掉部分上涨行情。某个阈值附近必然存在一个“收益/回撤”的甜点区找到它比调模型的任何超参数都值钱。6.2 波动率过滤能救回你的模型声誉股票预测模型在震荡市里会频繁发出错误信号。处理方案是在信号之上叠加波动率过滤条件——波动过高或过低的时期不交易。实现时只需要在回测循环里加一个条件当日波动率高于过去60日均值的1.5倍时强制空仓。df[vol_regime] df[volatility_20] (df[volatility_20].rolling(60).mean() * 1.5)原因是高波动期往往伴随重大利好利空此时价格由情绪驱动模型在当前特征维度下不稳定。加入这个过滤后回测胜率通常会上升几个点资金曲线的毛刺也会减少。6.3 验证模型有没有真正学到的三张图最后一步是画图验证。第一张画训练集和验证集的特征重要性对比看前五位特征是否一致第二张画预测概率分布图看模型输出是集中在0.5附近还是两边分叉第三张画对应真实涨跌的累积分布曲线。第三张图如果预测概率高的区间对应真实上涨比例也高说明模型确实学到了结构值得继续往下做。如果概率越高、真实上涨比例反而越低那就是特征构建过程中出现了问题优先检查泄漏。本文还有配套的精品资源点击获取