简介:Python基于机器学习的量化投资策略项目,适合毕业设计、期末大作业与课程设计场景,完整覆盖数据获取、特征工程、模型训练、策略回测和结果可视化等环节。压缩包共15个文件、1.14MB,其中5个Python脚本分别对应数据下载、特征构建、模型预测、主流程和回测;6张图表展示柱状图、K线、炒股界面、最大回撤及决策树预测股价涨幅等结果;2个文本文件提供股票列表与依赖约束,另有docx操作手册辅助部署。代码中的注释对新手友好,可以按main、feature、model、backtest的顺序理解量化策略从数据到验证的完整链路;同时提供tushare token配置、股票列表与依赖清单,便于快速接入数据并复现结果。目前已有286人学习使用,下载后简单配置即可运行,也适合用于答辩演示或作为量化入门参考。
1. 用 Python 把量化策略跑通:这份机器学习源码包能帮你省掉哪些事
量化的第一道坎不是策略,而是把数据、特征、模型、回测这条链路完整拼起来。这份基于 Python 和机器学习的量化投资策略源码包,恰好就是针对这个痛点来的:它从 Tushare 拉历史行情,用特征工程加工出均线、波动率、量比这类因子,再交给决策树预测股价涨幅,最后在 backtest.py 里跑回测,输出资金曲线、最大回撤和 K 线图。对正在做毕业设计、课程设计或者期末大作业的人,这是一个能直接当主线骨架的完整项目,代码带注释、目录规整,数据到模型到回测的每一步都看得见。你不用从零开始写数据清洗和回测框架,把精力放在理解策略逻辑和调参上,比对着空文档死磕效率高得多。
2. 项目结构与数据链路:从源码包目录到 Tushare 取数
2.1 源码包结构与各模块职责
先说目录。源码包解压后主目录下是这几个文件,我把职责列在表格里,方便你定位改哪里:
| 文件 | 职责 |
|---|---|
| main.py | 主入口,串联取数→特征→模型→回测 |
| data.py | 行情数据获取与缓存,封装 Tushare 接口 |
| feature.py | 特征工程:均线、收益率、波动率、量比等 |
| model.py | 决策树模型训练与预测 |
| backtest.py | 回测引擎:计算净值、最大回撤、年化收益 |
| stock_list.txt | 股票池,按行放 tushare 标准代码 |
| requirements.txt | Python 依赖清单 |
| 手册.1.docx | 使用说明与实验报告参考 |
| img/ | 运行截图:K线图、柱状图、最大回撤、炒股界面等 |
这个结构是典型的"数据-特征-模型-回测"四段式,也是量化项目最常见的工程划分。main.py 只做编排,真正的逻辑拆在 feature 和 model 里——这样做的好处是,你后面想换模型、换数据源,都只用动对应的模块,backtest.py 的净值计算逻辑可以原样复用。
我把 stock_list.txt 特意放在最后说,因为它决定了数据下载的股票池范围。一般格式是每行一个代码,像 000001.SZ、600519.SH 这样的 tushare 标准代码。如果你想控制运行时间,第一件事就是把这里面的股票数量往下降,先拿 2-3 只跑通全流程,再扩大到全池子。
2.2 Tushare 数据获取与 token 配置
data.py 里封装的取数逻辑,核心就是 tushare 的 pro 接口。第一次运行之前,需要先完成 token 初始化。常见做法是去 tushare.pro 注册账号,在个人主页拿到一串 token,然后在代码里调用 ts.set_token()。项目里那张 tushare_token.png 就是作者给你对照看的,token 该填在什么位置一目了然。
import tushare as ts # 设置 token:在 tushare.pro 个人主页获取,本质是一串密文 ts.set_token('你的token粘贴到这里') pro = ts.pro_api() # 按股票代码和起止日期拉日线 df = pro.daily(ts_code='000001.SZ', start_date='20200101', end_date='20231231') print(df.head())这里有几个参数值得强调。ts_code 是 tushare 标准代码,格式是"代码.交易所",SZ 是深交所、SH 是上交所,写错直接返回空数据;start_date 和 end_date 要求是 YYYYMMDD 格式的字符串,不是 datetime 对象,传错类型会直接报错。返回的 DataFrame 里,trade_date 字段是 int 类型的日期,比如 20231231,做排序和索引之前最好先统一转成字符串。
还有一个容易踩的坑:pro.daily() 返回的是未复权行情,遇到分红除权,股价会产生跳空,直接拿它算收益率会把除权当成大跌。我一般会在特征工程之前就解决复权问题,要么用前复权口径的数据,要么单独拉复权因子,具体操作放到第 5 章细说。
data.py 里通常还会做一层本地缓存,把拉下来的数据存成 csv 或 pickle。这个设计很实用——Tushare pro 接口对调用频率有限制,每次都全量重拉,既慢又容易触发限流。你改成自己代码的时候,这层缓存建议保留,它可以让你反复调参时不用重复等接口。
2.3 特征工程:feature.py 里在算什么
这一节是整个机器学习部分的地基。feature.py 的作用是把原始 OHLCV 数据加工成模型能吃的特征矩阵,同时生成训练用的标签。项目里预测的对象是"股价涨幅",所以标签是未来一段时间的收益率,而不是简单的涨跌分类。
import pandas as pd def make_features(df): df = df.sort_values('trade_date').copy() # 收益率:当日收盘相对昨收的变化 df['ret'] = df['close'].pct_change() # 简单均线:5 日和 10 日均线,捕捉中期趋势 df['ma5'] = df['close'].rolling(5).mean() df['ma10'] = df['close'].rolling(10).mean() # 量比:当日成交量 / 5 日均量,反映资金活跃度 df['vol_ratio'] = df['vol'] / df['vol'].rolling(5).mean() # 波动率:20 日收益标准差,刻画风险水平 df['volatility'] = df['ret'].rolling(20).std() # 标签:下一交易日收益率,用 shift(-1) 把未来挪到今天这一行 df['label'] = df['ret'].shift(-1) return df.dropna()参数含义要讲清楚。rolling(5) 和 rolling(10) 是滑动窗口大小,对应 5 日均线和 10 日均线,窗口越大曲线越平滑、对短期波动越迟钝;volatility 用 20 日窗口,这是金融里计算历史波动率的常见默认值。最后一行 dropna() 会把序列开头没有足够窗口的数据全部丢掉,因为前几行的 rolling 结果是 NaN,模型不能吃空值。
最关键的逻辑在 label 那一行。shift(-1) 表示把"明天的收益率"往前挪一天,让今天的特征对应明天的结果,这样模型学到的是"今天的特征 → 明天的涨跌"。方向不能搞反,如果写成 shift(1),等于用未来的特征预测过去的收益,整个训练集是错位的,后面回测再好看也是废的。
特征数量不用贪多。ret、ma5、ma10、vol_ratio、volatility 已经能组成一个可解释的因子集。想加特征的话,常见方向是 RSI、MACD 这类技术指标,但每加一个就要考虑计算窗口和缺失值,特征间的相关性也会涨。决策树对冗余特征还算耐受,但特征多了,训练时间和过拟合风险会一起上来。
3. 模型与策略落地:决策树预测股价涨幅与买卖信号生成
3.1 为什么选决策树:可解释性与小样本友好
model.py 用的模型是决策树,这不是随便选的。量化策略里模型的可解释性很重要——回测亏了、实盘翻了车,你得能说清楚是哪些因子、哪些阈值导致的。决策树天然能把决策路径可视化,每一条分裂规则都可以翻译成"如果量比大于某值且均线多头,则预测上涨",这种透明性是神经网络给不了的,在答辩和报告里也特别好讲故事。
另一个原因是小样本友好。训练数据本质上是"一只股票的历史行情",样本量通常只有几千行,特征也就个位数。这个规模下,XGBoost、深度学习反而容易过拟合;决策树配合合适的深度限制,能在小样本上训出稳定的效果。课程设计里展示决策树的分裂路径,也比展示一个黑匣子神经网络更让导师信服。
model.py 里常见的做法是把"预测涨幅"当成回归问题或分类问题。回归版本直接输出下一天的预测收益率;分类版本则把收益按正负切成上涨/下跌两类。项目截图里写的是"决策树预测股价涨幅",本质上是回归思路,但信号生成阶段依然是按预测涨跌幅的正负来转成买卖指令。这种基于机器学习的量化投资方法,核心就是让模型自己从历史数据里找特征与未来收益的关系,而不是靠人定死板的均线金叉规则。
3.2 训练与预测:model.py 的建模流程
from sklearn.tree import DecisionTreeRegressor def train_model(df): feature_cols = ['ret', 'ma5', 'ma10', 'vol_ratio', 'volatility'] X = df[feature_cols].values y = df['label'].values # 按时间顺序切分,前 80% 训练、后 20% 测试 split_idx = int(len(df) * 0.8) X_train, X_test = X[:split_idx], X[split_idx:] y_train, y_test = y[:split_idx], y[split_idx:] # 限制深度和叶子节点数,防止学到噪声 model = DecisionTreeRegressor(max_depth=5, min_samples_leaf=30) model.fit(X_train, y_train) return model这段代码有两个关键点。第一是切分方式,我这里没有用 train_test_split 的默认随机切分,而是按时间顺序做了 8:2 的硬切分。金融数据是强时序依赖的,随机切分会把未来的数据混进训练集,让模型"偷看"未来,回测结果虚高——这是初学者最容易翻车的点,没有之一。第二是模型参数,max_depth=5 限制树深度不超过 5 层,min_samples_leaf=30 要求每个叶子节点至少有 30 个样本,这两个参数直接控制模型复杂度。深度太深、叶子太小时,决策树会去记每一天的噪声,训练集完美、测试集惨败。
预测阶段就很简单了,拿到训练好的模型,对全量特征调用 predict,得到每一行的预测涨幅。这里训练集和全量数据用的是同一套特征列,顺序不能乱,否则 predict 的数据是错位的。
3.3 从预测到信号:main.py 的编排逻辑
main.py 是整个项目的主流程,职责是把前面的模块串起来,再把预测结果转成策略信号。信号规则是策略的核心决策逻辑,通常写成下面这样。
import numpy as np def generate_signal(df, model): pred = model.predict(df[feature_cols].values) df['pred'] = pred # 预测涨幅大于 0 则持仓,否则空仓 # 信号仅依赖当日可得特征,不引用任何未来信息 df['signal'] = np.where(df['pred'] > 0, 1, 0) return df信号生成有两个细节值得注意。第一是阈值选择,pred > 0 表示明天预测上涨就持仓,否则空仓。如果你想过滤掉更多噪声,可以把阈值提高到 0.005,要求预测涨幅超过 0.5% 才买入,代价是会错过一部分小幅上涨。第二是信号只依赖当日特征算出的预测值,这一点是回测可信的前提,第 4 章的回测代码里还会再做一层滞后对齐。
main.py 的运行顺序一般是:先 data.py 拿数据,再 feature.py 造特征,接着 model.py 训练,最后 generate_signal 生成信号并交给 backtest.py。建议第一次跑不要直接执行整个 main.py,先打开文件看一遍调用顺序,再逐个 import 对应函数,定位问题会快很多。
另外提一下 img 目录里的"炒股界面.png",它其实是回测完成后用 matplotlib 把净值曲线和买卖点画在 K 线图上的效果,不是实时交易的终端界面。这个误会挺常见的,先铺垫一下,下一章展开回测和可视化。
4. 回测框架与风险指标:最大回撤怎么算、怎么读
4.1 backtest.py 到底做了什么
回测是所有量化项目里最不能含糊的一环。backtest.py 做的事情是:拿到上一章生成的信号,模拟从第一天到最后一天的持仓过程,算出每天的账户净值,再汇总成关键指标。
def run_backtest(df): # 策略收益 = 昨日信号 * 今日收益率 # 避免用当天信号去成交当天收益:那属于未来函数 df['strategy_ret'] = df['signal'].shift(1) * df['ret'] # 净值曲线:每天累乘 df['net_value'] = (1 + df['strategy_ret']).cumprod() # 基准:买入持有 df['buy_hold'] = (1 + df['ret']).cumprod() # 年化收益:按 252 个交易日折算 total_ret = df['net_value'].iloc[-1] - 1 days = len(df) annual_ret = (1 + total_ret) ** (252 / days) - 1 return df, annual_ret这段代码的核心就是 signal.shift(1)。假设你在 5 月 10 日收盘后根据特征算出买入信号,这个信号最早只能在 5 月 11 日执行。不做这层 shift,直接拿当天信号乘当天收益,等于假设你能在信号产生的同一刻成交,这在现实中不存在。回测里但凡出现"收益高得离谱"的曲线,第一怀疑目标就是这个。
252 是 A 股一年的交易日数量近似值,年化收益就是用这个数字折算的。如果你换了市场,或者数据里夹着非交易日,这个数字要对应调整,不然年化收益会失真。
4.2 最大回撤和夏普比率:风险指标的读法
项目里单独放了一张"最大回撤.png",说明作者把最大回撤当成核心风险指标。最大回撤描述的是:从某个净值高点跌到之后的最低点,账户最多亏了多少。这个数字直接关系到策略能不能拿得住。
def max_drawdown(net_value): # 截至当天为止的历史最高净值 rolling_max = net_value.cummax() # 回撤 = 当前净值相对历史高点的跌幅 drawdown = net_value / rolling_max - 1 return drawdown.min()cummax() 计算到当天为止的净值最高点,net_value / rolling_max - 1 得到的是一个负数,比如 -0.18,含义是从高点回撤了 18%。最大回撤越大,策略的波动越剧烈——即使最终赚钱,中间的过程也可能让你在最低点割肉离场。一个回撤 40% 的策略和一个回撤 10% 的策略,即使年化收益相同,实际持仓体验完全不一样。
做毕业设计时,建议把这几个指标都放进报告里:
| 指标 | 计算口径 | 怎么读 |
|---|---|---|
| 累计收益 | 期末净值 - 1 | 看策略整体赚没赚钱 |
| 年化收益 | 按 252 交易日折算 | 不同策略横向对比 |
| 最大回撤 | 净值高点至后续低点的最大跌幅 | 越小说明风险控制越好 |
| 夏普比率 | (策略收益 - 无风险利率) / 收益波动率 | 每单位风险换来多少超额收益 |
夏普比率在这个项目里不是重点,但答辩时老师很爱问。它的核心思想是看收益的"质量":年化 20% 但曲线乱蹦,和年化 15% 但曲线平滑,后者的夏普可能反而更高,因为分母里的波动率更小。
4.3 回测可视化:K 线图、柱状图和资金曲线
img 目录下的几张图对应回测输出的不同视图。k_chart.png 是带买卖点的 K 线图,柱状图.png 一般是每日收益或回撤分布,最大回撤.png 画的是回撤曲线,炒股界面.png 则是把净值曲线、收益柱状图、交易信号组合在一个画布里的总览。
常见做法是用 matplotlib 的 subplots 一次铺四张子图:最上面是 K 线加买卖点标记,第二张是净值曲线 vs 基准收益,下面两张分别是每日收益柱状图和回撤曲线。如果想省事,可以用 mplfinance 专门画 K 线,但它对数据列名有要求,必须把列改成 Open/High/Low/Close 的标准命名,报错时先检查这一层。
我一般会在回测输出里固定打印四行数字:累计收益、年化收益、最大回撤、交易次数。交易次数特别容易被忽略但极其关键——信号天天翻转说明策略噪声大,每次调仓都要付手续费和滑点,交易成本能把一个原本盈利的策略拖成亏损。
5. 避坑指南:环境部署、数据权限与回测里的五个常见翻车点
5.1 现象:pip install 后 import 直接报 ModuleNotFoundError: No module named 'tushare'
原因:只装了 tushare 一个包,没装全依赖。这个项目的核心依赖是 pandas、scikit-learn、matplotlib、tushare,而且新版本 Python 环境里 scikit-learn 的 API 和旧版本有差异,缺一环就挂。
解决:严格按 requirements.txt 安装,装完先做一次 import 冒烟测试,确认四个核心包都能进来,再跑 main.py。
pip install -r requirements.txt python -c "import tushare as ts; import sklearn; import pandas; import matplotlib; print('ok')"这条命令一次验证四个依赖,任何一个缺失都会在这一步报出来,而不是等 main.py 跑到一半才炸。requirements.txt 本身是对应作者当时的版本环境写的,如果你用的是最新版 Python,个别包可能需要手动调整版本号,但先按清单装是成本最低的起点。
5.2 现象:pro.daily() 报错"抱歉,您没有访问该接口的权限"
原因:tushare 的 pro 接口按用户积分分级,新注册账号积分不够,daily 接口权限没开。
解决:两个办法。一是换成老接口 ts.get_k_data(),它不需要积分,适合先把流程跑通;二是去 tushare.pro 完善资料、积攒积分,等权限开了再切回 pro 接口。建议前期用 get_k_data 跑通整个项目,后面做正式报告时再切 pro 接口,两者返回的数据列名略有差异,切换时记得更新 feature.py 里的字段名。
提示:不管用哪个接口,token 不要硬编码在代码里提交到公开仓库,容易泄露,我一般放到本地配置文件或环境变量里。
5.3 现象:回测年化收益 300% 以上,曲线美得不像真的
原因:未来函数,而且是最常见的两种。一是信号没做 shift,用当天信号成交当天收益;二是标签生成时把未来信息卷进了特征。这个项目原版代码结构是没问题的,但你自己改动后很容易在这两个位置出岔子。
解决:从头排查两行代码。backtest.py 里 signal 必须 shift(1);feature.py 里 label 用 shift(-1)。任何看起来要用未来数据的指标,都要确认它在当天收盘那一刻是拿不全的。检查方法很简单:把信号曲线和 K 线叠在一起看,如果买入点总是出现在大涨当天的最低价附近,几乎可以断定有未来函数。
5.4 现象:训练集得分 98%,测试集和回测却亏损
原因:过拟合,决策树深度太大、叶子节点太少,把历史噪声当成了规律。
解决:把 max_depth 压到 3-5,min_samples_leaf 调到 20-50,用交叉验证看稳定性。我的习惯是先把深度压到 3,如果回测还能赚钱,再逐步放开;一旦测试集表现跟着恶化,立刻退回上一档参数。决策树这种模型,复杂度和泛化能力是直接对着干的,不要追求训练集上的高准确率。
5.5 现象:除权日出现巨大跳空,均线被拉断,买卖信号频繁翻转
原因:用未复权数据算均线和收益率。分红送股后股价向下跳空,策略误判为大跌,触发一堆错误信号。
解决:取数时用前复权口径。tushare 里可以用 ts.pro_bar(adj='qfq') 直接拿前复权数据,或者用 pro.adj_factor() 拉复权因子自己折算。关键是整个链路要统一:特征、回测、K 线图必须用同一套复权价格,混着用就会对不上,回测里会出现假信号。
提示:复权方式必须全局统一,中途换口径等于换了数据源,前后结果没有任何可比性。
6. 从复现到改进:换模型、滚动验证与实盘前的检查清单
6.1 把决策树换成随机森林或 LightGBM
模型替换是这个项目里性价比最高的改进。单棵决策树容易过拟合,随机森林通过多棵树投票缓解这个问题,LightGBM 在梯度提升框架下效果更强、但参数也更多。
from sklearn.ensemble import RandomForestRegressor model = RandomForestRegressor(n_estimators=200, max_depth=5, min_samples_leaf=30) model.fit(X_train, y_train)n_estimators 是树的数量,200 是个常见起点,再大训练变慢但精度提升有限;max_depth 和 min_samples_leaf 沿用决策树的那套经验,控制复杂度。替换后重点关注训练集和测试集得分的差距:差距缩小说明泛化变好;如果测试集不升反降,问题不在模型,要回去查特征和标签。
6.2 用滚动窗口替代单次切分
单次 8:2 切分只能证明策略在某一小段历史上有用,换成滚动回测才能验证稳定性。推荐用 sklearn 的 TimeSeriesSplit。
from sklearn.model_selection import TimeSeriesSplit tscv = TimeSeriesSplit(n_splits=5) for train_idx, test_idx in tscv.split(X): X_train, X_test = X[train_idx], X[test_idx] model.fit(X_train, y_train) # 记录每次测试集的收益和回撤,观察稳定性n_splits=5 表示切 5 段,每段都用前面的样本训练、后面的样本验证。这五次结果如果忽好忽坏,说明策略对时间段极其敏感,这种策略放到实盘里大概率亏钱。
6.3 实盘前的三件事
回测通过只是第一步,我自己的习惯是再做三件事。第一,把手续费和滑点加进回测,A 股双边手续费加上滑点按 0.1%-0.2% 估算,高频调仓的策略会被这笔成本吃掉一大截。第二,看调仓频率,一周只交易一两次的策略比每天翻仓的策略抗造得多。第三,用小资金跑一段模拟盘,验证数据源、信号计算和下单流程在真实环境里没出岔子,再考虑放大资金。
这个项目作为毕业设计或课程设计的骨架是够用的,你完全可以把 6.1 和 6.2 的改进写进论文的实验章节,答辩时这就是加分项。
说实话,我第一次跑通回测看到年化 200% 的成绩时也兴奋过,后来把信号 shift 一步加上,收益直接掉到 30%,那一刻才真正理解了"未来函数"这四个字的杀伤力。从那以后,我每次拿到回测曲线,第一件事就是检查信号有没有对齐、标签有没有泄题,先排雷再看收益。希望这份避坑经验能帮到你,少走点我当年走过的弯路。
本文还有配套的精品资源,点击获取