ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

决策树量化投资回测实战:从数据到模型

决策树量化投资回测实战:从数据到模型 简介这是一套基于Python与机器学习的量化投资策略完整项目适合毕业设计、期末大作业或课程设计场景也面向想快速上手量化回测的初学者。项目从数据获取、特征构建、模型训练到策略回测均有相应模块支撑并配有清晰代码注释与说明文档部署门槛低。压缩包共15个文件以.py源码为核心覆盖主程序、特征处理、回测与建模等环节另含6张可视化图表用于展示行情K线、最大回撤、决策树预测涨幅等结果以及技术手册、依赖清单和股票列表等辅助文件整体仅1.14MB。该套资源已经过导师认可目前已有285人学习除完整可运行代码外还给出回测逻辑与结果图方便读者快速理解策略效果并作二次扩展。1. 先想清楚机器学习在量化投资里到底预测什么很多人在打开这个 Python 项目时第一反应是这里头是不是藏了一个深度神经网络在预测股价。实际上跑一遍 main.py 就会发现核心模型是 sklearn 的决策树分类器解决的是一个二分类问题明天相对今天收盘价是涨还是跌。整个项目围绕「数据获取 → 特征构造 → 模型训练 → 回测评估」四段式展开数据源走 tushare回测部分手写交易信号和资金曲线最大回撤、累计收益都是自己算的没有依赖 backtrader 这类重型框架。适合三类人拿它做毕业设计和课程设计的学生、想入门量化但不想一开始就啃框架的新手、以及想快速跑通一个「特征模型回测」最小闭环的从业者。2. 数据管道不是黑匣子从 tushare 拉到本地 CSV 的完整链路2.1 先看文件职责表五个 py 文件在管线里的分工在跑任何代码之前先花几分钟把文件结构理清楚。这个项目解压后核心是五个 py 文件和一个 docx 使用说明加上 stock_list.txt 和 img 目录。我拿到手一般的习惯是按执行顺序读代码而不是按文件名顺序读因为 main.py 作为总入口已经把流程串起来了。文件职责依赖data.py从 tushare 拉取日线行情并缓存tushare, pandasfeature.py计算技术指标特征生成特征矩阵和标签列data.py 的输出model.py训练决策树分类器并评估feature.py 的输出backtest.py按预测信号模拟交易计算回测指标model.py 的预测结果main.py串联 data→feature→model→backtest输出图表以上全部这个文件划分本身就是一个标准的量化项目骨架。你在答辩时如果被问「整个系统怎么组织的」照着这条数据流讲比背整份代码要清晰得多。requirements.txt 管依赖.gitignore 管忽略缓存文件img 目录存运行后生成的柱状图、K 线图、回撤曲线等这些都属于工程化的标配也在侧面说明项目不是随手写的。2.2 data.py 与 stock_list.txttoken 是第一个门槛data.py 干的事情就是通过 tushare pro 的 daily 接口拉日线行情。tushare 的绝大多数数据接口都需要先在官网注册拿到 token然后在代码里 set_token这个 token 相当于你访问数据接口的钥匙。项目里放了一张 tushare_token.png 截图就是提示你 token 填在哪个位置避免新手卡在第一步。import tushare as ts import pandas as pd # 在 tushare.pro 注册后在个人主页复制 token 粘贴到下面 ts.set_token(在这里填你的token) pro ts.pro_api() def read_stock_list(pathstock_list.txt): 读取股票池每行一个代码支持 000001.SZ 这种带后缀的格式 with open(path, r, encodingutf-8) as f: codes [line.strip() for line in f if line.strip()] return codes def fetch_daily(ts_code, start_date20200101, end_date20241231): 拉取单只股票的日线数据并做基础清洗 df pro.daily(ts_codets_code, start_datestart_date, end_dateend_date) if df is None or df.empty: return None df[trade_date] pd.to_datetime(df[trade_date]) df df.sort_values(trade_date).reset_index(dropTrue) return df代码逻辑说明read_stock_list 把 stock_list.txt 里的代码逐行读出来过滤掉空行fetch_daily 调用 tushare 的 daily 接口拿日线返回的字段包含 trade_date、open、high、low、close、pre_close、pct_chg、vol、amount 等。注意我把 trade_date 先转成 pandas 的 datetime 类型再排序这一步很关键后面所有 rolling、shift 操作都建立在日期升序的基础上如果顺序是乱的均线和动量算出来全是错的。参数说明start_date 和 end_date 按需求改。我的建议是回测区间拉 3 到 5 年太短样本凑不够太长数据里包含牛熊切换对模型的适应性要求更高。tushare 的 daily 接口对单次调用有行数限制积分越高单次能拉的越多如果一次拉不全要分段落拼接。vol 单位是手amount 单位是千元这两个字段在算量比和成交额特征时要留意单位换算。2.3 feature.py 的第一层转换原始行情到特征矩阵feature.py 从原始 OHLCV 派生出特征列。我拆过不少同类毕设特征基本围绕动量、均线关系、量能变化三个方向设计这个项目也走的是这个路子。关键边界在于特征只能用 T 日及之前的数据这一条如果破了整条回测曲线都是假的后面第 5 章会专门展开。def build_features(df): df df.copy() df[ret_5] df[close] / df[close].shift(5) - 1 # 5日动量 df[ma5] df[close].rolling(5).mean() # 5日均线 df[ma20] df[close].rolling(20).mean() # 20日均线 df[ma_bias] df[close] / df[ma20] - 1 # 乖离率 df[vol_ma20] df[vol].rolling(20).mean() df[vol_ratio] df[vol] / df[vol_ma20] # 量比 df[amplitude] (df[high] - df[low]) / df[close] # 当日振幅 # 标签次日收盘价是否高于今日涨为 1跌为 0 df[label] (df[close].shift(-1) df[close]).astype(int) df df.dropna().reset_index(dropTrue) return df feature_cols [ret_5, ma_bias, vol_ratio, amplitude]代码逻辑说明shift(5) 表示把整个序列往后移 5 位第 T 行拿到的就是 T-5 日的收盘价rolling(5).mean() 是过去 5 天的均值注意它是包含 T 日的所以均线本身不算未来函数。label 用 shift(-1) 把次日涨跌这个「结果」对齐到当前行模型学的是给定今天能看到的信息明天更可能涨还是跌。参数说明窗口大小 5 和 20 对应周线和月线级别的短期趋势是技术分析里的常见配置。想验证趋势策略可以再加 ret_20、ma60 这类长周期特征但滚动窗口越大dropna 后丢的样本越多。3 年日线大约 700 多个交易日窗口设到 60 就可能丢掉近 10% 的样本数据不够时模型容易不稳定。amplitude 用收盘价做分母是为了让高价股和低价股的振幅可比否则 100 块的股票和 5 块的股票振幅绝对值不在一个量级。3. 决策树的真正用法特征、标签与参数怎么咬合3.1 标签为什么是 T1 涨跌方向而不是具体价格训练模型之前先回答一个问题为什么标签是涨跌方向而不是预测明天收盘价是多少。金融序列预测里回归比分类难得多预测价格误差两毛钱也是方向对了但 MSE 会把你罚得很难看把问题简化成「涨还是跌」评估指标变成准确率和混淆矩阵答辩时好解释策略逻辑也清晰。决策树分类器正好适合这个二分类问题。从实操角度看真实交易系统的第一层本来就是方向判断第二层才是仓位管理。你先解决方向问题再谈收益率。价格预测即使做准了还要面对滑点和手续费侵蚀而方向判断的容错空间明显更大。这也是为什么大多数基于机器学习的量化策略课程、毕业设计第一步都是构建涨跌分类器。3.2 决策树选型理由与核心参数max_depth、min_samples_split、random_state为什么是决策树而不是多层感知机或者别的高阶模型一是数据量决定的3 年单只股票的日线只有 700 个样本深度学习在这个规模下基本学不出泛化能力二是可解释性决策树可以画成树状图每一层用什么特征、切分阈值是多少都看得见这在课程设计和答辩里有天然优势三是决策树对特征尺度不敏感不需要做标准化量比、乖离率这些特征可以直接进模型。from sklearn.tree import DecisionTreeClassifier from sklearn.model_selection import train_test_split X df[feature_cols] y df[label] # 对金融时序数据切分时不能随机打乱按时间顺序前 70% 训练后 30% 测试 split_idx int(len(df) * 0.7) X_train, X_test X.iloc[:split_idx], X.iloc[split_idx:] y_train, y_test y.iloc[:split_idx], y.iloc[split_idx:] model DecisionTreeClassifier( max_depth4, # 限制树的深度防止过拟合 min_samples_split30, # 内部节点再划分所需最小样本数 min_samples_leaf15, # 叶子节点最少样本数 random_state42 # 固定随机种子保证结果可复现 ) model.fit(X_train, y_train) print(训练集准确率:, model.score(X_train, y_train)) print(测试集准确率:, model.score(X_test, y_test))代码逻辑说明这里没有用默认的 train_test_split而是先算出切分位置再用 iloc 按行号切。原因在后面 3.3 细说。模型训练完打印两个准确率训练集和测试集的差距就是判断过拟合最直接的依据。参数说明max_depth4 是控制模型复杂度的核心旋钮深度越大树越复杂训练集准确率很快能到 90% 以上但测试集会往下掉min_samples_split 和 min_samples_leaf 是防止树在样本极少的位置继续切分相当于给分裂操作加了下限。日线数据本身噪声极大纯技术指标能做到 55% 到 60% 的方向准确率已经不错如果测试集准确率冲到 70% 以上先怀疑数据泄漏而不是觉得模型变强了。3.3 时序切分shuffleFalse 和手工切分的区别sklearn 的 train_test_split 默认 shuffleTrue会先把数据随机打乱再切分。对普通机器学习任务这没问题但金融时序数据一旦打乱训练集里就混进了「未来」的样本模型相当于开卷考试回测准确率虚高一到实盘立刻现原形。# 错误写法shuffle 默认 True金融时序数据千万别这么切 # X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.3) # 正确写法一按位置手工切分保证训练集全在时间上早于测试集 split_idx int(len(df) * 0.7) X_train, X_test X.iloc[:split_idx], X.iloc[split_idx:] y_train, y_test y.iloc[:split_idx], y.iloc[split_idx:] # 正确写法二如果你需要 K 折用 TimeSeriesSplit from sklearn.model_selection import TimeSeriesSplit tscv TimeSeriesSplit(n_splits5)代码逻辑说明手工切分是最直观的时序分割前 70% 样本做训练后 30% 做测试两者在时间上完全不重叠。TimeSeriesSplit 是 sklearn 专门给时序数据准备的分割器它保证每一折的训练集索引都小于验证集索引适合做参数调优时的交叉验证。我在复现完以后习惯加一个断言来验证切分正确性打印 X_train.index.max() 和 X_test.index.min()确认训练集的最后一行在位置上确实早于测试集的第一行。这个习惯帮我抓出过好几次因为排序不当导致的隐性泄漏。4. 回测的四个关键细节信号移位、最大回撤、基准对比与可视化4.1 交易信号必须 shift今天算信号、明天才执行backtest.py 是这套代码里最容易出彩也最容易翻车的地方。很多毕设回测曲线好看得离谱原因就出在信号对齐上。模型在 T 日收盘后根据特征算出 signal但这个信号真正能成交是在 T1 日开盘中间隔了一个夜间的消息面变化所以执行必须往后挪一天。import pandas as pd def run_backtest(df, model, feature_cols): df df.copy() X df[feature_cols] df[signal] model.predict(X) # 今天收盘后算出的信号 df[position] df[signal].shift(1) # 明天开盘才执行 df[daily_ret] df[close].pct_change() df[strategy_ret] df[position] * df[daily_ret] df[cum_return] (1 df[strategy_ret]).cumprod() running_max df[cum_return].cummax() df[drawdown] df[cum_return] / running_max - 1 max_drawdown df[drawdown].min() return df, max_drawdown代码逻辑说明signal 在 T 日收盘产生position 用 shift(1) 整体后移一天所以 T 日的 position 实际是 T-1 日的信号。每天的策略收益等于当天持仓方向乘当天的涨跌幅如果模型预测当天上涨position 为 1多头吃到涨幅预测下跌position 为 0空仓躲过跌幅。这里有一个关键理解如果你把 shift 去掉直接用 df[signal] 去乘 df[daily_ret]当天信号是收盘后产生的当天收益已经走完回测里等于用未来信息交易曲线会漂亮到失真。这是回测里最经典的未来函数问题后面第 5 章再展开排查方法。4.2 三个必看指标累计收益、最大回撤、胜率回测不能只看最终净值至少要看三个指标累计收益、最大回撤、胜率。项目里最大回撤.png 这张图对应的就是资金曲线和回撤区间的可视化。def evaluate(df): total_return df[cum_return].iloc[-1] - 1 years len(df) / 250 # A股一年约 250 个交易日 annual_return (df[cum_return].iloc[-1]) ** (1 / years) - 1 win_count (df[strategy_ret] 0).sum() trade_count (df[position] ! 0).sum() win_rate win_count / trade_count if trade_count else 0 return { 累计收益: round(total_return, 4), 年化收益: round(annual_return, 4), 胜率: round(win_rate, 4), 最大回撤: round(df[drawdown].min(), 4) }代码逻辑说明累计收益是策略净值的最终倍数减 1年化收益用几何年化也就是累计收益开 1/years 次方而不是简单除以年份这样更符合复利逻辑。胜率这里用正收益天数除以持仓天数代表模型看对方向的频率。参数说明250 是一年交易日的近似值A 股实际每年约 242 到 250 个交易日用 250 计算年化不影响对比。最大回撤是资金曲线从阶段高点跌下来的最大幅度它比累计收益更值得关注——一段回撤超过 30% 的策略实盘里绝大多数人拿不住。项目里柱状图.png 应该就是每日或每月的收益分布你可以从中看出收益是否集中在少数几天如果大部分收益由几天贡献策略的稳健性存疑。4.3 图表输出验证柱状图、K 线图和炒股界面各说明什么项目 img 目录里已经有跑完后的输出图用来验证策略行为和答辩展示柱状图.png策略每日或每月的收益分布图。如果看到收益集中在一两个交易日说明策略实际靠运气而不是模型判断力。k_chart.pngK 线图上标注了模型的买入信号和卖出信号这是答辩时的主图直观展示模型在哪些位置看多、哪些位置看空。最大回撤.png资金曲线加回撤阴影展示风险控制能力的核心证据。炒股界面.png项目提供的可视化界面把信号、净值、持仓放在一起演示时比甩一堆数据表格有说服力。需要注意的是这些图表如果出现中文乱码去看第 5.4 的中文字体配置。图形输出本身不是目的它是用来回答「模型凭什么赚钱」的只会跑数据而讲不出图表含义答辩时容易被追问。5. 避坑实录这个项目最容易翻车的五个位置5.1 tushare token 无效与接口限流现象运行 data.py 直接报「抱歉您没有访问该接口的权限」或者 token 验证失败。原因tushare 的 daily 接口需要账户积分达标才能调用新注册账号积分不够接口直接拒绝另一种可能是 set_token 里粘贴的字符串带了空格或换行符token 解析失败。解决先确认 token 字符串前后没有多余字符再登录 tushare.pro 在个人主页查看积分。积分不够时优先用数据量要求低的接口测试或者按官方文档完成实名认证提升积分。项目里的 tushare_token.png 截图就是提醒你 token 填在哪别跳过这一步直接跑后续代码。5.2 特征 NaN 引发训练崩溃现象model.py 跑训练时报 ValueError: Input contains NaN或者准确率打印出来是 0。原因rolling、shift 在序列开头会产生 NaNfeature.py 里 dropna 只执行了一次但如果你中途加了新特征、改了窗口大小dropna 的时机可能没覆盖到所有列另外停牌日数据缺失pct_change 也会产生 NaN。解决训练前强制加一行print(df[feature_cols].isnull().sum())检查每个特征列的空值数量。如果只是行首的 NaN把 dropna 放在特征全部构造完之后如果是中间日期的缺失用df df.ffill()前向填充再用 dropna 收尾。我一般会在特征函数返回前加断言assert df[feature_cols].isnull().sum().sum() 0确保数据干净进入模型。5.3 回测好看但实盘失效未来函数与过拟合现象回测年化收益 30%实盘跑两周连续止损资金曲线和回测完全对不上。原因八成是未来函数。最常见的就是 4.1 说的情况——信号没有 shift当天收盘算出的信号直接乘当天收益或者用了包含当日收盘后的信息去回测。剩下两成是过拟合决策树 max_depth 调得太深把噪声也学了进去。解决先检查回测主循环里有没有 shift(1)把信号执行日强制后移一天再用 3.3 的时序切分重新评估。如果确认没有未来函数就把 max_depth 降到 3 或 4min_samples_leaf 调大宁可训练集准确率低 5 个点也要保住测试集和回测的稳定性。判断过拟合还有一个经验测试集准确率和训练集差距超过 15 个百分点基本就是过拟合。5.4 matplotlib 中文乱码现象柱状图、炒股界面图上所有中文标题和标签变成方块。原因matplotlib 默认字体里没有中文字体Windows 下最常见Mac 和 Linux 也偶尔遇到。解决在绘图代码开头强制指定字体两行配置即可。SimHei 在 Windows 通常可用如果机器没有黑体换成 Microsoft YaHei 或系统自带的其他中文字体。import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False5.5 停牌复牌造成收益跳变现象回测净值曲线上出现单日大幅跳变K 线图上某天涨跌幅异常大导致指标失真。原因股票停牌多日复牌后波动叠加到一天pct_change 算出极端值或者数据源某天没有行情记录rolling 窗口的计算位置错位均线被拉偏。解决数据清洗阶段先对 trade_date 做去重和补全。去重是防止 tushare 重复返回同一天的记录补全是对停牌日做前向填充保留行位置但不更新价格。如果某只股票长期停牌更干脆的做法是直接从 stock_list.txt 里移除这只股票回测不把它纳入股票池。停牌股的收益跳变会严重拉高最大回撤但它不是策略本身的风险。6. 让决策树开口说话把预测逻辑导出成可交付的材料模型跑完不能只报一个准确率。答辩和实际交付的时候老师或同事更想看「模型到底根据什么判断涨跌」。决策树相比神经网络最大的优势就是可解释性要把这个优势用起来。用 sklearn 的 tree 模块可以一键导出树结构图。from sklearn.tree import export_graphviz import graphviz dot_data export_graphviz( model, out_fileNone, feature_namesfeature_cols, class_names[跌, 涨], filledTrue, roundedTrue, proportionTrue ) graph graphviz.Source(dot_data) graph.render(decision_tree, formatpng, cleanupTrue)跑完会生成 decision_tree.png从图上能直接看出模型第一层用哪个特征做分裂、切分阈值是多少、每个叶子节点的样本占比和预测类别。我在项目里见过很好的演示方式是打印一张 A3 的决策树贴到文档里讲模型如何用「量比大于 1.3 且乖离率小于 -0.02 就预测上涨」这种人类能读懂的规则做决策比空说准确率直观得多。如果环境里没有 graphviz也可以用 matplotlib 直接画树from sklearn.tree import plot_tree import matplotlib.pyplot as plt fig, ax plt.subplots(figsize(20, 10)) plot_tree(model, filledTrue, feature_namesfeature_cols, class_names[跌, 涨], axax) plt.savefig(decision_tree_plot.png, dpi200, bbox_inchestight)复现时我还有个习惯main.py 跑完后手动写一段验证逻辑随机抽 30 个测试集样本打印「实际涨跌 vs 模型预测」的对比表再确认回测的总交易天数和累计收益能对上账。这个项目从结构上看是基于「特征 决策树 手写回测」的最小量化闭环参数和阈值都在源码里可以直接改说明文档也给了部署步骤。我当时跑这类量化项目第一遍时最深的教训是回测好看不算本事把每一步的中间结果打印出来、让逻辑看得见才算真正跑懂了。从那以后我每拿到一份量化数据都强制自己先打印 df.head() 和标签分布再进模型。希望帮到你。本文还有配套的精品资源点击获取
返回列表