简介:基于机器学习的量化投资策略示例程序,面向有一定Python基础、但对炒股和量化投资尚不了解的初学者,以A股市场为例,演示从数据获取、特征构建、模型训练到策略回测的完整流程。压缩包共14个文件,以5个脚本为主干,分别负责数据处理、特征工程、模型构建、回测与主程序调度;辅以2个说明文档、6张结果图表和配置文件,整体仅727KB,结构清晰,便于按模块研读。已有156人学习下载。借助该示例可直观理解量化投资‘跟随趋势而非创造趋势’的核心思想,掌握线性回归、决策树等机器学习算法在股价涨跌预测中的应用,并学会通过回测评估策略、结合最大回撤与K线图观察盈亏表现,同时建立止损、仓位控制等风险管理意识,是快速上手量化策略开发的实用入门资料。
1. 从「看不懂K线」到跑通第一个机器学习量化策略:这份 demo 源码到底能干什么
先交代一个反直觉的结论:这份资源不是给你一个「稳赚代码」,而是一张让你从零看懂量化投资全流程的「地图」。项目标题里的机器学习、量化投资策略、demo 源码三个词,对应的是 A 股日线数据、特征工程、决策树模型、回测脚本这一整条链路。它适合有一定 Python 基础、但完全没接触过炒股和量化的人:你能在本地跑通数据拉取、特征构建、模型训练、回测出图,看到净值曲线、最大回撤和预测准确率这些量化里的核心指标到底长什么样。
很多人第一次接触量化,容易陷入「找一个神奇指标、一把梭哈」的误区。这个 demo 的价值恰恰相反:它把数据、特征、模型、回测四块拆成独立文件,让你明白哪一步出了问题、哪一步决定了最终收益。你不需要有金融背景,只需要会装依赖、能看懂 pandas 和 sklearn 的代码。下面我从工程角度把这套源码的每个文件、每条命令、每个坑都过一遍。
2. 源码结构与运行准备:先搞懂每个文件在整条链路里的位置
2.1 文件清单与职责划分
拿到压缩包解压后,核心文件是这几个:main.py、data.py、feature.py、model.py、backtest.py,外加stock_list.txt、requirements.txt和.gitignore。任何一个合格的量化项目,代码都必须按「数据 → 特征 → 模型 → 回测」分层,这份 demo 的分层很清楚,这也是我推荐新手直接读它的原因。
各文件的典型职责是这样的:
| 文件 | 职责 | 关键产出 |
|---|---|---|
data.py | 调用 tushare 拉取日线行情,做基础清洗 | 个股 OHLCV DataFrame |
feature.py | 基于历史行情构造特征列 | 特征矩阵 X 与标签 y |
model.py | 训练决策树/随机森林分类模型 | 训练好的模型对象 |
backtest.py | 用历史数据模拟买卖,计算收益曲线 | 净值序列、最大回撤 |
main.py | 串联上述模块,生成图表 | 回测图、柱状图、K 线图 |
stock_list.txt存放股票代码池,比如000001.SZ、600000.SH这样的格式,main.py会逐行读取,再交给data.py去拉数据。.gitignore里通常忽略tushare_token和临时数据文件,这是项目作者刻意留下的好习惯——token 属于敏感信息,不该入库。
2.2 环境安装与 token 配置
先看requirements.txt,这份 demo 的依赖并不复杂。一般会包含pandas、numpy、scikit-learn、matplotlib、tushare这几个库。我建议你使用虚拟环境安装,避免污染系统 Python:
cd ai_qua_demo python -m venv .venv source .venv/bin/activate # Windows 下执行 .venv\Scripts\activate pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后,下一步是配置 tushare token。tushare 是国产免费财经数据接口,注册后在个人主页能看到一串 token。你需要在data.py里找到类似pro.set_token('你的token')的位置替换掉。这里有个实际经验:token 不要硬编码在代码里,更不要提交到 git。常见做法是放到环境变量里读取,或者单独存一个本地配置文件并加入.gitignore:
import os import tushare as ts # 从环境变量读取 token,没有则读取本地配置文件 token = os.environ.get("TUSHARE_TOKEN") if not token: with open("tushare_token", "r") as f: token = f.read().strip() ts.set_token(token) pro = ts.pro_api()2.3 数据拉取:从 stock_list 到干净的日线表
data.py的核心逻辑是遍历股票池,逐只拉取前复权日线数据。前复权很重要,因为 A 股有分红送转,不复权数据会让价格出现断崖式跳空,直接影响收益率计算。拉取后做三件事:按日期排序、去重、删除停牌导致的空行。
def fetch_daily(code: str, start: str = "20220101") -> pd.DataFrame: df = pro.daily(ts_code=code, start_date=start) if df is None or df.empty: return pd.DataFrame() df = df.sort_values("trade_date").reset_index(drop=True) # 只保留需要的列,减少内存占用 cols = ["trade_date", "open", "high", "low", "close", "vol", "amount"] return df[cols]这里有一个新手容易忽略的细节:pro.daily返回的日期是整数格式(如20220101),排序前要确认类型一致,否则字符串和整数混排会得到错误顺序。另外,如果 tushare 积分不够,daily接口可能限流,建议在代码里加一个 0.5 秒的 sleep 再请求下一只股票。
数据到位后,下一步就是特征工程——这是整个策略里对最终收益影响最大、也最容易犯错的一环。
3. 特征工程与标签构造:为什么预测「涨跌方向」而不是「涨跌数值」
3.1 原始行情到特征矩阵的转换
feature.py做的事情,是把上一章拿到的 OHLCV 数据变成机器学习能吃的表格。量化里最常用的特征类别有四类:动量类(过去 N 日涨幅)、均线类(价格与均线的偏离度)、波动率类(过去 N 日收益标准差)、量能类(当日成交量与均量的比值)。这份 demo 的特征设计基本围绕这几类展开,代码逻辑如下:
def build_features(df: pd.DataFrame) -> pd.DataFrame: df = df.copy() # 动量:5 日与 20 日累计涨幅 df["ret_5"] = df["close"].pct_change(5) df["ret_20"] = df["close"].pct_change(20) # 均线偏离度:收盘价相对 20 日均线的位置 df["ma20"] = df["close"].rolling(20).mean() df["close_ma20_ratio"] = df["close"] / df["ma20"] - 1 # 波动率:20 日收益标准差 df["volatility_20"] = df["close"].pct_change().rolling(20).std() # 量能:当日成交量 / 5 日均量 df["vol_ratio"] = df["vol"] / df["vol"].rolling(5).mean() # 标签:未来 5 个交易日收益率是否为正(二分类) df["label"] = (df["close"].shift(-5) / df["close"] - 1 > 0).astype(int) return df.dropna()特征构造有几个关键点在代码里没直接写,但你必须理解,否则后面模型训练全是虚的。
第一,pct_change默认是相对于前一行的变化率,但传入参数后就是多日累计变化率。close.pct_change(5)表示「5 天前的收盘价到今天的变化比例」,这是一个强动量信号。
第二,label用shift(-5)把未来 5 日的收益搬到当前行,这是时间序列预测的标准做法。它的含义是:用今天及以前的信息,预测未来 5 天的涨跌。千万别搞成shift(5),那是用未来预测过去,模型在回测里会「开天眼」,实盘立刻翻车。
第三,dropna()会把最早的 20 行删掉,因为滚动窗口不够。数据集会变小,这是正常的,但要注意训练集和测试集的样本量是否还够。
3.2 训练集与测试集划分:只能用时间切,不能用随机切
模型训练里最忌讳的一点,是在金融数据上使用train_test_split(random_state=42)这种随机划分方式。原因很直接:股票数据是时间序列,相邻日期的样本高度相关。随机划分会让模型中「偷看」到未来信息,回测结果虚高到离谱。
正确做法是按时间顺序切分:
split_idx = int(len(df) * 0.8) train_df = df.iloc[:split_idx] test_df = df.iloc[split_idx:] X_train = train_df[feature_cols].values y_train = train_df["label"].values X_test = test_df[feature_cols].values y_test = test_df["label"].values以一只股票 500 个交易日的数据为例,前 400 天做训练,后 100 天做测试。这样回测时模型看到的未来,是真正意义上的「未来」。如果样本不够,宁可减少特征数量、缩小回看窗口,也不要用随机切分。
3.3 特征与标签的对齐关系:最容易出 bug 的时序边界
我见过非常多新手在特征对齐上翻车。举一个具体场景:df["ret_5"]是用close.pct_change(5)算的,这一行在第 100 行的值,用到的是第 95 天的收盘价——信息全部来自过去,没问题。但如果你在计算过程中不小心用了shift(-1)之类的操作,让某一行特征包含次日数据,整个训练集就被污染了。
一个简单可靠的自检方法:训练完成后,打印特征矩阵的末尾日期和标签的末尾日期,确认标签列的时间戳晚于特征列的时间戳。代码里可以加一行断言:
assert len(train_df) + len(test_df) == len(df)这句断言保证训练集和测试集加起来恰好等于全量数据,没有重叠也没有遗漏。如果断言失败,说明中间某处改变了行数,需要回头检查 dropna 或合并操作。
4. 模型构建与训练:决策树、随机森林和参数选择的实战取舍
4.1 为什么用决策树而不是线性回归或神经网络
这份 demo 的核心模型是决策树,配套可能还有一个随机森林。为什么不选线性回归?因为股票特征和未来涨跌之间基本不是线性关系——5 日涨幅在 0% 附近,市场情绪的作用机制和涨幅在 20% 时完全不同。为什么不选神经网络?因为 A 股单只股票的日线样本只有几百条,神经网络在这种小样本、高噪声的数据上很容易过拟合,训练速度还慢。
决策树的优势在于:可解释性强,每个分裂条件都能还原成「涨幅大于 3% 且量比大于 1.2 时看多」这样的业务规则;对特征尺度不敏感,不需要做标准化;训练速度快,几百条样本瞬间完成。它的劣势是容易过拟合,所以必须限制树的深度。model.py里典型做法是:
from sklearn.tree import DecisionTreeClassifier from sklearn.ensemble import RandomForestClassifier def train_model(X_train, y_train): # 限制深度防止过拟合,min_samples_leaf 保证叶子节点样本量 model = DecisionTreeClassifier( max_depth=4, min_samples_leaf=20, class_weight="balanced", random_state=42 ) model.fit(X_train, y_train) return model4.2 参数怎么调:max_depth、min_samples_leaf 和 class_weight
决策树有五个常用参数,这里只讲 demo 里最关键的三个。
max_depth=4表示树最多分裂 4 层。深度太深,模型会把训练集里的噪声全背下来;深度太浅,又学不到有效规律。4 层对应最多 16 个叶子节点,每个叶子代表一类「特征组合下的涨跌概率」。min_samples_leaf=20强制每个叶子至少包含 20 个样本,进一步防止过拟合。调参时有个经验法则:叶子节点样本数不要低于总样本的 2%。
class_weight="balanced"处理涨跌样本不均衡。A 股长期看是牛短熊长,一段时间内下跌天数可能明显多于上涨天数,如果不做均衡处理,模型会倾向把所有样本都预测成下跌,回测曲线变成一根直线。
随机森林版本就是多棵决策树的集成:
rf = RandomForestClassifier( n_estimators=100, max_depth=4, min_samples_leaf=20, n_jobs=-1, random_state=42 )n_estimators=100表示 100 棵树,每棵树用随机抽样的子集训练,最终投票决定涨跌。n_jobs=-1让所有 CPU 核心并行训练,几百条样本毫无压力。随机森林通常比单棵决策树稳健,但代价是特征重要性被平均化,不太容易看出具体规则。
4.3 模型评估:准确率不是唯一指标,还要看混淆矩阵
demo 里会输出模型在测试集上的预测准确率,以及绘制决策树预测股价涨幅的相关图表。但我要提醒你:准确率这个指标在量化场景下只作参考,不要当真事。假如测试集里 60% 的样本是上涨,模型什么都不学、全部预测上涨,准确率就有 60%。这类「虚高准确率」掩盖了模型的真实区分能力。
更值得看的是混淆矩阵和预测为正的准确率:
from sklearn.metrics import confusion_matrix, classification_report y_pred = model.predict(X_test) print(classification_report(y_test, y_pred)) tn, fp, fn, tp = confusion_matrix(y_test, y_pred).ravel() # 预测上涨且确实上涨的比例(精确率) precision_positive = tp / (tp + fp) if (tp + fp) else 0一个正常有效的策略,预测上涨的精确率应该在 52%~58% 之间。高于 60% 要怀疑数据泄漏,低于 50% 说明模型没有学习能力,不如直接猜。这个区间是量化投资里的一个经验常识,因为日线级别的股票收益信噪比极低,单日预测精确率能稳定超过 55%,扣掉手续费后已经算优秀策略。
5. 回测与结果解读:backtest.py 如何把模型预测变成收益曲线
5.1 回测的核心逻辑:信号、仓位、成交价
模型训练完,最重要的是用历史数据验证它能不能赚钱。backtest.py承担这件事。回测的基本逻辑不复杂:每天根据模型输出一个二进制信号(1 表示预测未来上涨,0 表示预测下跌),信号为 1 时持有股票,为 0 时空仓,最后按日计算组合净值。
成交价是回测里最容易被钻空子的地方。有人用当日收盘价成交,但这意味着你「收盘时已经知道模型结果」——如果模型用的是当日收盘数据,那确实可以这样模拟,但现实中你在收盘前最后一刻才拿到预测,成交价滑点无法避免。更保守的做法是信号当日生成、次日开盘价成交:
def run_backtest(df, signal, initial_cash=1000000): df = df.copy() df["signal"] = signal df["position"] = df["signal"].shift(1) # 次日开盘执行 df["market_return"] = df["close"].pct_change() # 扣除双边手续费 0.1% 和卖出印花税 0.05% df["strategy_return"] = df["position"] * df["market_return"] df["strategy_return"] = df["strategy_return"] - 0.0001 * df["position"].diff().abs() df["equity"] = initial_cash * (1 + df["strategy_return"]).cumprod() return df5.2 最大回撤与净值曲线的计算
main.py运行后,会产生一组图表,其中最关键的是三张:净值曲线图、最大回撤图、K 线与买卖点叠加图。最大回撤.png展示的是策略从净值最高点到最低点的最大跌幅,这是衡量风险最直观的指标。计算逻辑如下:
def max_drawdown(equity_series: pd.Series) -> float: cumulative_max = equity_series.cummax() drawdown = equity_series / cumulative_max - 1 return drawdown.min()比如初始资金 100 万,策略净值最高到 120 万,后来跌到 96 万,最大回撤就是96/120 - 1 = -20%。这个数字的意义在于:回撤 20%,你需要涨 25% 才能回到原来的净值。很多新手只看累计收益,忽略回撤,实盘中回撤超过心理阈值会导致提前止损出局。
5.3 回测结果怎么算「有效」:横向比较与基准对照
回测跑完,不能只看净值涨了就说策略有效。至少要对比两个基准:一是买入持有(从第一天全仓买入,一直不动),二是沪深 300 指数同期涨幅。把策略净值曲线和买入持有净值画在同一张图里,如果策略收益跑不赢买入持有,那这个模型就没有存在的意义。
柱状图.png在这份 demo 里通常展示的是策略月度收益分布,可以用来观察收益是否集中在某几个月。如果 12 个月里 6 个月赚钱 6 个月亏钱,但总收益为正,说明策略在不同市场状态下都有一定适应力。如果收益全部来自某一段单边上涨行情,那本质上是「牛市里买什么都涨」。
5.4 交易成本与滑点:为什么回测盈利实盘亏损
这是回测环节最容易被忽略、也最致命的细节。很多人跑完 demo,看到年化 30% 就觉得找到财富密码了,但实盘一跑全是亏损。原因几乎都出在没扣成本。A 股的实际成本包括:佣金(万 2.5 到万 3,最低 5 元)、印花税(卖出时收 0.05%)、滑点(买卖价差,一般按 0.1% 估计)。如果策略每天交易一次,一年 250 个交易日,双边成本大概是 250 * 0.2% = 50% 的年化成本。任何日均换手率高的策略,第一件事就是压低交易频率,否则再好的预测模型也会被手续费吃光。
6. 避坑指南:数据泄漏、tushare 限流和难以置信的高收益
6.1 现象:回测准确率高达 90%,实盘完全不是这么回事
这是量化新手最容易遇到也最兴奋的「假象」。当你看到模型准确率超过 90% 时,先别急着截图发朋友圈。90% 准确率在金融时间序列上几乎不可能,出现这个数字只有一种解释:代码里某个地方用了未来数据。最常见的是shift()方向搞反,或者特征计算时用了包含当天的滚动窗口,而标签也是当天数据,导致特征和目标之间产生了信息重叠。
解决方法是立刻做一件事:把训练和预测的时间边界画出来,检查每一列特征的「最后已知日期」是否都早于标签日期。另外,用简单模型做二次验证——如果决策树都能到 90%,很可能是数据泄漏;因为金融数据信噪比极低,真实场景下任何模型都很难稳定超过 60%。
6.2 现象:tushare 报错「抱歉,您没有访问该接口的权限」
刚配置好 token,运行data.py就报权限错误。原因在于 tushare 的多数 pro 接口有积分门槛,新注册用户默认积分较低,只能访问基础的daily接口,而daily_basic(每日指标)可能需要 2000 积分以上。
解决思路有三个:一是检查当前 token 对应的积分等级,先用pro.daily(ts_code=code, start_date=...)拉最基础的行情数据,不要再调用高级接口;二是到 tushare 官网完成实名认证和积分任务,提高权限;三是在代码里做降级处理,拉不到日线指标就用收盘价自己计算均线和波动率,demo 本来也不需要太多衍生指标。
6.3 现象:回测净值曲线很漂亮,但换一只股票就彻底失效
这通常不是数据问题,而是模型过拟合到某只股票的特性。决策树分裂条件可能学到「当收盘价 > 10 元且成交量 > 500 万手时上涨」,这个规律只适用于代码里那一只股票,换一只 20 元的股票立刻失效。
处理办法是交叉验证。把股票池里的股票分成两组,一组训练模型参数,另一组做样本外验证。如果样本外表现和样本内差距巨大,说明模型过拟合,应该减少特征数量、加大min_samples_leaf、或者直接上随机森林让多棵树投票来抹平单只股票的噪声。demo 的价值本来就定位在「学习流程」,而不是给你一个能直接实盘盈利的策略,这个预期要摆正。
6.4 现象:数据里出现未来日期导致的回测「幻觉收益」
有一种隐蔽的 bug 是日期没有排序、或者复权因子计算错误,导致某几行数据的时间顺序颠倒。回测时shift(1)会错误地把「未来一天的收益」算进当天。这种错误在净值曲线上看起来像日内突然暴拉,其实是跨天错位。
我的自查习惯很简单:回测前先打印df[["trade_date", "close", "position", "strategy_return"]].head(20),人工确认几行数据的因果关系是否成立——position 是前一天算出来的,strategy_return 是当天真实发生的,两者顺序不能反。虽然麻烦,但这几步能救回无数个假装有效的假策略。
7. 进阶玩法:把 demo 从「跑通」变成「能用的研究框架」
7.1 滑点与交易成本建模
backtest.py里的成本模型非常粗糙,只按固定比例扣费。想让回测更接近实盘,至少要区分买入佣金和卖出印花税,还要加一个固定成本项——每次交易最低 5 元佣金。一个更实际的成本函数可以长这样:
def calc_cost(trade_price: float, trade_value: float, is_sell: bool) -> float: commission = max(trade_value * 0.00025, 5.0) stamp_tax = trade_value * 0.0005 if is_sell else 0.0 slippage = trade_value * 0.001 return commission + stamp_tax + slippage把这个函数嵌入回测循环后,再重新看年化收益,通常会被吃掉 3~8 个百分点。只有扣完这些成本仍然为正的策略,才值得继续研究。
7.2 Walk-forward 验证:用滚动窗口代替一次性切分
我前文一直强调训练集和测试集要按时间切分,但一次性切分还有一个隐患:模型只在一个固定时间段上被验证,结论依赖运气。更稳的做法是 walk-forward,也叫滚动前向验证。原理是:用 2018-2022 的数据训练,预测 2023 年第一季度;再用 2018-2023Q1 训练,预测 2023Q2,每次训练窗口扩展、预测窗口前移,重复四轮。
for fold in range(4): train_end = "20230101" if fold == 0 else roll_dates[fold - 1] test_start = roll_dates[fold] test_end = roll_dates[fold + 1] model = train_model(train_filtered) test_pred = model.predict(test_filtered) # 保存每一折的收益与回撤四折结果如果方向一致、收益稳定,这个策略才具备基本的可信度。从那以后我每次跑完一个 demo 策略,都会被强制走一遍 walk-forward 验证流程,不经过这一步的结果一律视为「自嗨」。这个习惯帮我挡住过不少漂亮的过拟合曲线。
7.3 参数敏感性检查
最后给一个很实用的技巧:把max_depth从 2 调到 6、min_samples_leaf从 10 调到 50,分别记录回测年化收益和最大回撤,做成一个小表格。如果收益随参数变化剧烈摆动,说明策略不稳定;如果收益在一个宽参数范围内呈平台期,说明规律是真实存在的。这个习惯能让你在投入大量时间调参之前,先判断这个策略值不值得继续。
到这里,这套 demo 源码的核心价值也就讲透了:它不是一个躺赢的量化工具,而是一条让你亲手体验数据、特征、模型、回测四个环节的完整生产线。把每个环节的坑都踩一遍,你才真正算入了量化投资的门。希望帮到你。
本文还有配套的精品资源,点击获取