
简介基于机器学习的量化投资策略示例程序面向有一定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 DataFramefeature.py基于历史行情构造特征列特征矩阵 X 与标签 ymodel.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这几个库。我建议你使用虚拟环境安装避免污染系统 Pythoncd 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。常见做法是放到环境变量里读取或者单独存一个本地配置文件并加入.gitignoreimport 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_codecode, start_datestart) if df is None or df.empty: return pd.DataFrame() df df.sort_values(trade_date).reset_index(dropTrue) # 只保留需要的列减少内存占用 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_state42)这种随机划分方式。原因很直接股票数据是时间序列相邻日期的样本高度相关。随机划分会让模型中「偷看」到未来信息回测结果虚高到离谱。正确做法是按时间顺序切分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_depth4, min_samples_leaf20, class_weightbalanced, random_state42 ) model.fit(X_train, y_train) return model4.2 参数怎么调max_depth、min_samples_leaf 和 class_weight决策树有五个常用参数这里只讲 demo 里最关键的三个。max_depth4表示树最多分裂 4 层。深度太深模型会把训练集里的噪声全背下来深度太浅又学不到有效规律。4 层对应最多 16 个叶子节点每个叶子代表一类「特征组合下的涨跌概率」。min_samples_leaf20强制每个叶子至少包含 20 个样本进一步防止过拟合。调参时有个经验法则叶子节点样本数不要低于总样本的 2%。class_weightbalanced处理涨跌样本不均衡。A 股长期看是牛短熊长一段时间内下跌天数可能明显多于上涨天数如果不做均衡处理模型会倾向把所有样本都预测成下跌回测曲线变成一根直线。随机森林版本就是多棵决策树的集成rf RandomForestClassifier( n_estimators100, max_depth4, min_samples_leaf20, n_jobs-1, random_state42 )n_estimators100表示 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_cash1000000): 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_codecode, 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把这个函数嵌入回测循环后再重新看年化收益通常会被吃掉 38 个百分点。只有扣完这些成本仍然为正的策略才值得继续研究。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 源码的核心价值也就讲透了它不是一个躺赢的量化工具而是一条让你亲手体验数据、特征、模型、回测四个环节的完整生产线。把每个环节的坑都踩一遍你才真正算入了量化投资的门。希望帮到你。本文还有配套的精品资源点击获取