简介这份资源面向计算机、金融工程等专业的学生与量化入门者提供一套基于机器学习预测股票价格趋势的Python仿真课程设计项目可用于课程设计、期末大作业或自学练手。压缩包共3个文件包含1个py主程序、1个md说明文档和1个ico图标整体约10KB体量轻巧但结构完整下载后无需修改即可直接运行。目前已有746人学习下载说明其在同类课程设计资料中具有一定参考价值。项目围绕利用机器学习提升量化交易成功率的思路展开主程序实现了数据读取、特征处理、模型训练与趋势预测的完整流程配套文档则梳理了方法原理与实现要点便于读者理解建模逻辑并在此基础上替换数据集或调整模型参数。对于需要快速完成课程设计、又想掌握机器学习在金融场景落地思路的读者这份源码与数据可直接作为可运行、可复现的学习模板。1. 从一份课程设计压缩包说起机器学习预测股票趋势到底能跑出什么很多人第一次接触「基于机器学习实现预测股票价格趋势的Python仿真源码数据」这类课程设计是在期末周前两周老师给了一个模糊的题目要求交一份能跑的代码、一份数据、一份报告。你打开搜索引擎看到满屏的「机器学习入门」「Python教程」却找不到一份能直接跑通、又能讲清楚每一步为什么这么做的方案。这份压缩包真正要解决的问题不是「预测明天涨跌」——那是玄学——而是用一套完整的、可复现的流程把历史行情数据变成特征、把特征喂给模型、把模型输出变成可评估的趋势判断最后用回测告诉你这套东西到底有没有比抛硬币强。它适合三类人正在做课程设计、需要一份结构完整且能讲清楚原理的作业的学生想从零搭一个量化实验环境、但不想一上来就碰高频交易框架的开发者以及已经会调sklearn、但没认真处理过时间序列标签和回测偏差的从业者。这篇文章不承诺任何收益只讲怎么把这条流水线搭起来、参数怎么调、哪里最容易翻车。读完你应该能自己写出一份不丢人的仿真代码并且知道它的边界在哪。2. 数据、标签与特征把K线变成模型能吃的矩阵2.1 为什么不能直接用收盘价当标签股票价格预测最常见的翻车点是把「预测未来收盘价」当成回归任务然后用均方误差去衡量。价格本身是非平稳的今天 100 块明天 105 块模型只要输出昨天的价格就能拿到很低的误差但这对趋势判断毫无意义。更合理的做法是把问题转成分类预测未来 N 天的收益率方向或者预测未来 N 天收益率是否超过某个阈值。常见做法是取未来 5 个交易日的收益率大于 1% 标为 1上涨小于 -1% 标为 0下跌中间的不参与训练。这样标签更平衡也更贴近「趋势」这个词的本意。我一般会先算对数收益率而不是简单收益率因为对数收益率在时间上可加且对极端值更稳健。标签生成必须用shift(-N)向前看但要注意最后 N 行没有未来数据必须丢掉否则会引入未来函数。这一步是血泪经验很多人报告里准确率 90%就是因为没丢最后几行模型在训练时看到了未来。import pandas as pd import numpy as np # 假设 df 有 date, open, high, low, close, volume 列按日期升序 df df.sort_values(date).reset_index(dropTrue) # 对数收益率 df[log_ret] np.log(df[close] / df[close].shift(1)) # 未来 5 日累计对数收益率 N 5 df[future_ret] df[log_ret].shift(-1).rolling(N).sum().shift(-(N-1)) # 打标签涨过 1% 为 1跌过 1% 为 0中间丢弃 threshold 0.01 df[label] np.where(df[future_ret] threshold, 1, np.where(df[future_ret] -threshold, 0, np.nan)) # 丢掉没有标签的行 df df.dropna(subset[label]).reset_index(dropTrue)这段代码里N和threshold是两个必须调的参数。N太小比如 1 天噪声极大标签几乎随机N太大比如 20 天样本量骤减且趋势定义变得模糊。我一般从 5 开始试阈值从 0.5% 到 2% 之间扫一遍看标签分布是否接近 4:6 到 6:4。如果某一类占比低于 30%模型会偏向多数类准确率虚高。2.2 特征工程均线、波动率和量价关系特征决定了模型的上限。对日频数据我通常分四组构造趋势类不同窗口的移动平均线及其斜率、波动类滚动标准差、ATR、量价类成交量变化率、量价相关性、动量类RSI、过去 N 日收益率。不要一股脑把几十个特征全塞进去先做相关性筛选去掉相关系数高于 0.9 的冗余特征。课程设计里常见的一个错误是用了未来信息构造特征比如用当日收盘价算的均线去预测当日方向这属于数据泄露。# 趋势特征 for w in [5, 10, 20, 60]: df[fma_{w}] df[close].rolling(w).mean() df[fma_slope_{w}] df[fma_{w}].diff(5) / df[fma_{w}].shift(5) # 波动特征 df[vol_20] df[log_ret].rolling(20).std() df[atr_14] (df[high] - df[low]).rolling(14).mean() # 量价特征 df[vol_chg] df[volume].pct_change(5) df[price_vol_corr] df[log_ret].rolling(20).corr(df[volume].pct_change()) # 动量特征 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) # 去掉因滚动窗口产生的 NaN df df.dropna().reset_index(dropTrue)参数说明均线窗口 5/10/20/60 覆盖短中长周期斜率用 5 日差分衡量方向。波动率窗口 20 是常用月频近似。RSI 用 14 日这是 Wilder 原始设定。所有滚动计算都只用历史数据rolling默认右对齐不会看到未来。构造完特征后检查一下特征矩阵的列数通常 20 到 40 列比较合适太多容易过拟合太少模型学不到东西。3. 模型选型与训练从逻辑回归到梯度提升树的取舍3.1 为什么我优先用树模型而不是LSTM课程设计里很多人一上来就写 LSTM觉得深度学习更高级。但在几百到几千条日频样本上LSTM 参数量远大于样本量训练集损失降得很低验证集准确率却和随机猜差不多。传统机器学习模型里梯度提升树如 LightGBM、XGBoost在表格型金融特征上表现稳定训练快还能输出特征重要性方便你在报告里解释。逻辑回归可以作为基线如果树模型比逻辑回归提升不到 2 个百分点说明特征本身信息量有限换更复杂的模型也没用。我一般会按时间顺序切分数据前 70% 训练中间 15% 验证调参最后 15% 测试。绝对不能用随机切分因为时间序列的样本不独立随机切分会让未来数据泄露到训练集。sklearn的TimeSeriesSplit可以做滚动验证但课程设计里用简单的按时间切分就够关键是测试集必须是最新的一段模拟真实上线场景。from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline import lightgbm as lgb feature_cols [c for c in df.columns if c not in [date, label, future_ret]] X df[feature_cols].values y df[label].values.astype(int) # 按时间切分 n len(X) train_end int(n * 0.7) val_end int(n * 0.85) X_train, y_train X[:train_end], y[:train_end] X_val, y_val X[train_end:val_end], y[train_end:val_end] X_test, y_test X[val_end:], y[val_end:] # 逻辑回归基线 lr Pipeline([ (scaler, StandardScaler()), (clf, LogisticRegression(max_iter1000, class_weightbalanced)) ]) lr.fit(X_train, y_train) print(LR val acc:, lr.score(X_val, y_val)) # LightGBM clf lgb.LGBMClassifier( n_estimators300, learning_rate0.05, max_depth4, num_leaves15, min_child_samples20, subsample0.8, colsample_bytree0.8, class_weightbalanced, random_state42 ) clf.fit(X_train, y_train, eval_set[(X_val, y_val)], eval_metricbinary_logloss, callbacks[lgb.early_stopping(30)]) print(LGB val acc:, clf.score(X_val, y_val))参数说明max_depth4和num_leaves15是强正则防止树太深记住噪声。min_child_samples20保证每个叶子至少有 20 个样本。subsample和colsample_bytree做行和列采样进一步降方差。early_stopping(30)表示验证集损失 30 轮不降就停。class_weightbalanced处理标签不平衡。这些参数不是固定的你可以用验证集做一个小网格搜索但别在测试集上调参。3.2 训练完必须看的三个诊断指标准确率是最容易骗人的指标。如果标签里 60% 是上涨模型全猜上涨就有 60% 准确率。所以必须同时看召回率、精确率和 AUC。我一般要求验证集 AUC 至少 0.55测试集 AUC 不低于 0.52否则这套东西没有实用价值。另外要看特征重要性如果排名第一的特征是某个你没注意到的泄露变量比如未来收益率相关的列混进去了必须删掉重训。from sklearn.metrics import classification_report, roc_auc_score y_pred clf.predict(X_test) y_prob clf.predict_proba(X_test)[:, 1] print(classification_report(y_test, y_pred)) print(Test AUC:, roc_auc_score(y_test, y_prob)) # 特征重要性 importance pd.DataFrame({ feature: feature_cols, importance: clf.feature_importances_ }).sort_values(importance, ascendingFalse) print(importance.head(10))如果 AUC 在 0.5 附近徘徊先别急着换模型回头检查标签定义和特征构造。常见问题是标签阈值设得太窄导致正负样本高度重叠或者特征里混入了未来信息验证集虚高但测试集崩掉。我习惯把测试集 AUC 低于 0.52 的结果直接判定为「不可用」报告里如实写比硬吹准确率诚实得多。4. 回测与仿真把预测信号变成资金曲线4.1 用向量化回测快速验证信号质量训练出模型只是第一步真正决定这套东西有没有意义的是回测。课程设计里不需要写事件驱动引擎用向量化回测就够根据模型信号决定每天持仓1 为满仓0 为空仓计算策略日收益率累乘得到净值曲线。注意交易成本A 股单边按万分之三算加上滑点我一般设单边 0.1%。如果策略在扣费后净值曲线还跑不赢买入持有那它就没有 alpha。# 生成测试集信号 test_df df.iloc[val_end:].copy() test_df[signal] clf.predict(X_test) test_df[strategy_ret] test_df[signal].shift(1) * test_df[log_ret] test_df[strategy_ret] test_df[strategy_ret] - 0.001 * test_df[signal].diff().abs() # 净值曲线 test_df[nav] (1 test_df[strategy_ret]).cumprod() test_df[benchmark] (1 test_df[log_ret]).cumprod() # 关键指标 total_ret test_df[nav].iloc[-1] - 1 bench_ret test_df[benchmark].iloc[-1] - 1 sharpe test_df[strategy_ret].mean() / test_df[strategy_ret].std() * np.sqrt(252) max_dd (test_df[nav] / test_df[nav].cummax() - 1).min() print(f策略收益: {total_ret:.2%}, 基准收益: {bench_ret:.2%}) print(f夏普: {sharpe:.2f}, 最大回撤: {max_dd:.2%})signal.shift(1)是关键因为当天收盘后才知道信号最早第二天开盘才能交易。0.001 * signal.diff().abs()计算换手带来的成本信号从 0 变 1 或从 1 变 0 时扣一次。夏普比率用日收益年化sqrt(252)是一年交易日数。最大回撤看净值从峰值跌下来的最大幅度。如果夏普低于 0.5 或最大回撤超过 30%这套策略在实盘里基本拿不住。4.2 参数敏感性别让回测结果只在一个点上好看回测最容易犯的错是只跑一组参数看到夏普 1.5 就以为找到了圣杯。实际上稍微改一下标签阈值、持仓周期或交易成本结果可能完全反转。我一般会做三组敏感性测试标签阈值从 0.5% 到 2% 扫一遍持仓周期从 3 天到 10 天扫一遍交易成本从 0.05% 到 0.3% 扫一遍。如果策略只在某一组参数下表现好其他组都亏那就是过拟合不能要。results [] for th in [0.005, 0.01, 0.015, 0.02]: for n in [3, 5, 10]: # 重新生成标签和特征重新训练和回测 # 此处省略重复代码核心是记录每组参数的夏普和回撤 results.append({threshold: th, holding: n, sharpe: sharpe, max_dd: max_dd}) res_df pd.DataFrame(results) print(res_df.pivot(indexthreshold, columnsholding, valuessharpe))这张透视表能直观看出参数稳健性。如果夏普在相邻参数间波动超过 0.5说明策略对参数太敏感实盘里参数一漂移就失效。我通常只接受在至少 60% 参数组合下夏普大于 0.5 的策略否则宁可不用。课程设计里把这张表放进报告比只放一条净值曲线有说服力得多。5. 避坑与排查课程设计里最容易翻车的五个地方5.1 现象验证集准确率 85%测试集只有 50%原因最常见的是数据泄露。比如用df[close].rolling(5).mean()构造均线后没有shift(1)导致当日均线包含了当日收盘价而标签又是基于当日之后的数据。另一个可能是用了随机切分而不是时间切分未来样本混进了训练集。解决所有特征计算完后统一shift(1)确保 t 时刻的特征只用到 t-1 及之前的数据。切分必须按时间顺序测试集取最后一段。检查方法把特征矩阵和标签按时间画出来看特征是否在标签变化前就发生了跳变。5.2 现象模型预测全是 1 或全是 0原因标签不平衡太严重比如上涨样本占 80%模型发现全猜 1 就能拿高准确率。或者class_weight没设模型偏向多数类。解决先看标签分布如果某一类低于 30%要么调整阈值让分布更均衡要么用class_weightbalanced或 SMOTE 过采样。但金融数据里我一般不推荐 SMOTE因为合成样本会破坏时间结构用类别权重更安全。5.3 现象回测夏普很高但实盘一上就亏原因回测里用了收盘价成交但实际中收盘价你拿不到或者没扣交易成本频繁交易把利润全吃掉或者策略在测试集上过拟合参数是专门为这段数据调的。解决信号必须shift(1)用第二天开盘或收盘成交。交易成本按单边 0.1% 起步换手高的策略甚至要设 0.2%。参数调优只在验证集做测试集只用一次看完就封存。5.4 现象LightGBM 训练报错或警告刷屏原因特征里有 NaN 或无穷大。滚动窗口计算、pct_change、corr都可能产生 NaN 或 inf。LightGBM 对 NaN 有一定容忍但 inf 会直接报错。解决在dropna之后再加一步df df.replace([np.inf, -np.inf], np.nan).dropna()。另外检查volume是否为 0pct_change遇到 0 会变成 inf。数据清洗这一步不能省否则后面全是坑。5.5 现象特征重要性排名第一的是某个意料之外的列原因要么是泄露变量比如future_ret没从特征列里排除要么是某个特征和标签有机械关联比如用未来数据算的滚动统计。解决打印feature_cols列表逐个人工检查。任何包含future、label、target字样的列必须排除。如果某个特征重要性异常高比如是第二名的 10 倍大概率有问题删掉重训看 AUC 是否大幅下降如果下降很多说明之前的高 AUC 是假的。6. 进阶技巧用滚动训练和概率阈值提升仿真可信度课程设计里一次性训练、一次性测试的做法在真实场景里是不够的。市场风格会变三年前有效的特征今天可能失效。我一般会做滚动训练每 60 个交易日重新训练一次模型用最近 500 个交易日作为训练窗口预测接下来 60 天。这样模型能跟上市场变化回测结果也更接近实盘。实现上不复杂用一个循环按时间切片每次训练一个新模型把预测结果拼起来。window_train 500 window_test 60 step 60 all_preds [] for start in range(0, len(df) - window_train - window_test, step): train_slice df.iloc[start:start window_train] test_slice df.iloc[start window_train:start window_train window_test] X_tr train_slice[feature_cols].values y_tr train_slice[label].values.astype(int) X_te test_slice[feature_cols].values model lgb.LGBMClassifier(n_estimators200, max_depth4, num_leaves15, min_child_samples20, class_weightbalanced, random_state42) model.fit(X_tr, y_tr) preds model.predict_proba(X_te)[:, 1] all_preds.extend(preds) # 把预测概率拼回测试集按 0.5 或更高阈值生成信号另一个技巧是不直接用 0.5 作为分类阈值而是用验证集找最优阈值。比如模型输出概率你可以画一条精确率-召回率曲线选一个让精确率不低于 55% 的阈值。这样信号更少但更准交易成本也更低。我通常会把阈值定在 0.55 到 0.6 之间具体看验证集表现。技巧作用代价滚动训练适应市场变化减少过拟合计算时间增加代码复杂度上升概率阈值调优提高信号精确率降低换手召回率下降可能错过部分行情多模型集成降低单模型方差训练和推理时间翻倍特征稳定性筛选去掉随时间漂移的特征需要额外计算 PSI 等指标最后说一个我自己的习惯每次跑完回测我都会把净值曲线和基准曲线画在一张图上然后问自己一个问题——如果这是我自己的钱我敢不敢按这个信号下单如果答案犹豫那就说明回测里还有没暴露的问题。课程设计可以交差但别骗自己。希望帮到你。本文还有配套的精品资源点击获取