机器学习量化投资策略demo源码:从特征工程到回测避坑

发布时间:2026/10/11 22:36:58
机器学习量化投资策略demo源码:从特征工程到回测避坑 简介这是一份面向具备一定Python基础、对炒股与量化投资尚不熟悉的初学者的入门级demo源码围绕机器学习在A股量化策略中的应用展开帮助读者快速理解数据收集、特征工程、模型构建与回测评估的完整流程。资源包共14个文件以5个py脚本、6张png图表、2个txt说明及1个gitignore为主压缩包约727KB其中py文件分别承担回测、特征处理、模型训练与数据读取等模块职责png则直观呈现决策树预测涨幅、最大回撤、K线走势等结果。目前已有156人学习下载。通过这份源码读者可对照回测、特征工程与模型训练等核心环节理解线性回归、随机森林、支持向量机等算法在价格预测中的取舍并借助requirements.txt快速还原运行环境为后续自行搭建量化策略打下基础。1. 从一份 demo 源码说起机器学习做量化策略到底能跑出什么很多人第一次搜「基于机器学习的量化投资策略demo源码分享」心里想的其实是一件事我不想听概念我就想看到一份能跑起来的代码输入历史行情输出买卖信号最好还能回测出个净值曲线。这个诉求非常合理因为量化策略这东西光看公式永远看不出坑在哪只有把数据喂进去、把模型训出来、把回测跑出来你才会遇到那些真正的问题标签怎么打、特征怎么对齐、手续费怎么算、过拟合怎么发现。这份 demo 的定位不是帮你赚钱而是帮你把「机器学习 量化投资」这条链路完整走通一遍。它适合两类人一类是有 Python 基础、想从传统技术指标转向机器学习策略的开发者另一类是做过回测但没系统用过机器学习模型的交易爱好者。你不需要先精通金融工程但需要能看懂 pandas 的 DataFrame 操作知道什么是训练集和测试集。整条链路的核心是用历史量价数据构造特征用未来收益率构造标签训练一个分类或回归模型把预测结果转成仓位再用回测框架验证这套逻辑是否比随机猜强。我见过太多人卡在第一步拿一堆指标当特征拿当天涨跌当标签训出一个准确率 90% 的模型回测一跑亏得妈都不认识。问题不在模型在于标签和特征之间发生了未来函数泄漏。所以这份 demo 的重点不是模型多高级而是把数据处理的每个边界讲清楚让你知道哪些做法看起来对、实际上是在自欺欺人。2. 数据、特征与标签把机器学习的输入输出对齐到交易场景2.1 为什么量化策略的特征工程和普通机器学习不一样普通机器学习任务里特征和标签通常来自同一张表的不同列你不太需要担心时间顺序。量化投资完全不是这回事。金融时间序列有两个硬约束第一特征只能使用当前时刻及之前的信息任何用到未来数据的特征都会让回测结果虚高第二标签的定义直接决定了模型在学什么用未来 N 日收益率做标签和用未来 N 日涨跌方向做标签训练出来的模型行为差异巨大。常见做法是构造三类特征动量类过去 N 日收益率、均线偏离度、波动类过去 N 日波动率、ATR、量能类换手率变化、成交量比值。标签一般用未来 5 日或 10 日的收益率再做二分类涨/跌或三分类涨/平/跌。我一般会把标签做成未来收益率是否超过某个阈值而不是简单看涨跌因为 A 股大量交易日是小幅波动简单二分类会让模型学到一堆噪声。这里有一个容易被忽略的点特征和标签的窗口不能重叠。如果你用过去 20 日构造特征用未来 5 日构造标签那么第 t 日的样本和第 t1 日的样本在时间上高度相关。这会导致训练集和测试集如果随机划分信息会泄漏。正确做法是按时间顺序切分或者用 purged k-fold 这类方法把重叠部分剔除。2.2 用 pandas 构造特征和标签的最小代码下面这段代码演示从日线行情出发构造动量、波动、量能三类特征并生成未来 5 日收益率标签。数据假设是一个包含 date、open、high、low、close、volume 的 DataFrame。import pandas as pd import numpy as np def build_features_labels(df, label_days5, threshold0.02): df: 包含 date, open, high, low, close, volume 的 DataFrame按日期升序 label_days: 未来 N 日收益率作为标签 threshold: 三分类阈值收益率 threshold 为涨 -threshold 为跌其余为平 df df.copy() df[ret_1d] df[close].pct_change(1) df[ret_5d] df[close].pct_change(5) df[ret_20d] df[close].pct_change(20) # 均线偏离度 df[ma20] df[close].rolling(20).mean() df[ma60] df[close].rolling(60).mean() df[bias_ma20] (df[close] - df[ma20]) / df[ma20] df[bias_ma60] (df[close] - df[ma60]) / df[ma60] # 波动率 df[vol_20d] df[ret_1d].rolling(20).std() df[vol_60d] df[ret_1d].rolling(60).std() # 量能 df[vol_ratio_5_20] df[volume].rolling(5).mean() / df[volume].rolling(20).mean() df[turnover_change] df[volume].pct_change(5) # 标签未来 label_days 日收益率 df[future_ret] df[close].shift(-label_days) / df[close] - 1 # 三分类标签 df[label] 0 df.loc[df[future_ret] threshold, label] 1 df.loc[df[future_ret] -threshold, label] -1 # 去掉因滚动窗口和 shift 产生的空值 df df.dropna().reset_index(dropTrue) return df这段代码的关键参数有三个。label_days控制你预测的时间跨度5 日适合短线20 日适合中低频。threshold控制分类边界设得太小会让大量样本挤在中间类设得太大则正负样本极度不平衡。rolling窗口的长度决定了特征平滑程度窗口越长特征越稳定但越滞后。代码最后用dropna去掉空值注意这一步会损失前 60 行和后 5 行数据这是构造滚动特征和未来标签的必然代价。注意shift(-label_days)是未来函数的核心来源。如果你在构造特征时不小心用了shift(-1)之类的操作回测结果会好得离谱但实盘完全不能用。2.3 训练集、验证集、测试集的时间切分金融数据不能随机划分。我一般按 6:2:2 的时间顺序切分前 60% 做训练中间 20% 做验证调参最后 20% 做测试。如果数据量足够还会在训练集内部再做一次时间序列交叉验证。def time_split(df, train_ratio0.6, val_ratio0.2): n len(df) train_end int(n * train_ratio) val_end int(n * (train_ratio val_ratio)) train df.iloc[:train_end].copy() val df.iloc[train_end:val_end].copy() test df.iloc[val_end:].copy() feature_cols [c for c in df.columns if c not in [date, open, high, low, close, volume, future_ret, label]] X_train, y_train train[feature_cols], train[label] X_val, y_val val[feature_cols], val[label] X_test, y_test test[feature_cols], test[label] return X_train, y_train, X_val, y_val, X_test, y_test, feature_cols这里feature_cols的排除列表很重要。future_ret和label必须排除否则模型直接看到答案。date和 OHLCV 原始列也建议排除除非你明确想把价格本身作为特征。我见过有人把close当特征喂进去模型确实能学到东西但学到的是价格绝对水平换一段行情就失效。3. 模型训练与信号生成从 sklearn 到仓位映射3.1 选什么模型树模型、线性模型还是神经网络量化策略 demo 里最常见的模型是梯度提升树LightGBM、XGBoost和逻辑回归。树模型的优势是能自动处理特征非线性关系和缺失值对特征缩放不敏感训练速度快适合中小规模数据。逻辑回归的优势是可解释性强系数符号能告诉你每个特征的方向方便排查逻辑问题。神经网络不是不能用但在这个场景下性价比不高。金融数据信噪比极低样本量通常只有几千到几万条深层网络很容易过拟合。如果你非要用建议从单层或双层 MLP 开始加 dropout 和早停不要一上来就 LSTM 或 Transformer。我一般会先用逻辑回归跑一个 baseline再用 LightGBM 对比。如果 LightGBM 比逻辑回归好很多说明特征里有明显的非线性关系如果差不多说明线性模型已经够用没必要上复杂模型。3.2 训练 LightGBM 分类器并输出概率import lightgbm as lgb from sklearn.metrics import accuracy_score, classification_report def train_lgbm(X_train, y_train, X_val, y_val): # 将三分类标签映射到 0,1,2 label_map {-1: 0, 0: 1, 1: 2} y_train_mapped y_train.map(label_map) y_val_mapped y_val.map(label_map) params { objective: multiclass, num_class: 3, metric: multi_logloss, learning_rate: 0.05, max_depth: 5, num_leaves: 31, min_data_in_leaf: 50, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 5, verbose: -1, seed: 42 } train_data lgb.Dataset(X_train, labely_train_mapped) val_data lgb.Dataset(X_val, labely_val_mapped, referencetrain_data) model lgb.train( params, train_data, num_boost_round500, valid_sets[val_data], callbacks[lgb.early_stopping(50), lgb.log_evaluation(100)] ) return model, label_map参数说明learning_rate设 0.05 是为了配合早停如果设 0.1 收敛更快但容易过拟合。max_depth和num_leaves控制模型复杂度金融数据我一般把max_depth压在 4 到 6 之间num_leaves不超过 31。min_data_in_leaf设 50 是为了防止叶子节点样本太少导致过拟合。feature_fraction和bagging_fraction是行采样和列采样增加随机性来提升泛化。early_stopping(50)表示验证集损失 50 轮不下降就停止。训练完之后模型输出的是三个类别的概率。你需要把概率转成仓位。常见做法是如果上涨概率减去下跌概率大于某个阈值就做多如果下跌概率减去上涨概率大于阈值就做空或空仓否则不操作。def predict_position(model, X, label_map, long_threshold0.1, short_threshold0.1): proba model.predict(X) # proba 的列顺序对应 label_map 的值0 跌1 平2 涨 proba_up proba[:, 2] proba_down proba[:, 0] signal np.zeros(len(X)) signal[proba_up - proba_down long_threshold] 1 signal[proba_down - proba_up short_threshold] -1 return signallong_threshold和short_threshold是控制交易频率的关键参数。设得太小信号频繁翻转手续费吃掉利润设得太大信号太少资金利用率低。我一般会在验证集上网格搜索这两个参数目标不是最大化收益而是最大化夏普比率或卡玛比率。3.3 回测时最容易翻车的三个地方第一个是手续费和滑点。很多 demo 回测时假设零手续费结果实盘一跑发现收益全被吃掉。A 股单边手续费大约万二到万三加上印花税和滑点双边成本大约千一到千二。回测时至少按双边千一算。第二个是信号执行时间。如果你用当日收盘价计算特征和信号最早也只能在次日开盘成交。如果回测时用当日收盘价成交就是未来函数。正确做法是信号shift(1)后再计算收益。第三个是仓位管理。全仓进全仓出在回测里看起来很爽但实盘遇到连续亏损回撤会非常大。常见做法是固定比例仓位或者用波动率倒数加权让每笔交易的风险敞口大致相等。4. 避坑与排查那些让回测收益虚高的常见操作4.1 未来函数泄漏回测夏普 3.0实盘亏到怀疑人生现象回测净值曲线非常平滑夏普比率超过 2但实盘一跑完全不是那么回事。原因特征构造时用了未来数据。最常见的是用shift(-1)计算特征或者用全样本的均值和标准差做标准化。另一个隐蔽的来源是标签和特征窗口重叠导致训练集和测试集信息泄漏。解决所有特征只能用当前及之前的数据。标准化参数只能从训练集计算然后应用到验证集和测试集。切分数据时按时间顺序不要随机打乱。如果用了滚动窗口特征确保shift方向是正的。4.2 过拟合模型在训练集上准确率 90%测试集只有 50%现象训练集准确率远高于测试集或者验证集表现好但测试集差很多。原因模型太复杂、特征太多、样本太少。金融数据信噪比低几百个特征里可能只有几个真正有用剩下的都是噪声。解决减少特征数量用特征重要性筛选。降低模型复杂度减小max_depth和num_leaves。增加min_data_in_leaf。用早停。如果还不行说明这个预测目标本身可能就不可预测换标签定义或换预测周期。4.3 样本不平衡模型永远预测「平」准确率看着还行现象三分类里「平」的样本占 80% 以上模型学会了一直预测「平」准确率 80% 但完全没有交易信号。原因阈值设得太宽或者市场本身大部分时间在窄幅震荡。解决调整阈值让三类样本更均衡或者改用二分类只预测涨跌。用类别权重让模型更关注少数类。评估指标不要只看准确率要看 F1、AUC 或者直接看策略回测收益。4.4 回测参数过拟合调了 100 组参数总有一组好看现象在历史数据上网格搜索参数找到一组夏普 2.5 的参数实盘完全不行。原因参数搜索本身就是一种过拟合。你试的参数越多越容易找到在历史上恰好表现好的组合。解决限制参数搜索空间不要试太多组合。用样本外数据验证或者用 walk-forward 分析。如果一组参数只在特定时间段有效换一段时间就失效说明它没有稳健性。4.5 忽略交易成本回测年化 30%扣掉手续费只剩 5%现象策略换手率很高回测收益看起来不错但加上手续费后大幅缩水。原因回测时没有正确计算交易成本或者低估了滑点。解决回测时按双边千一到千二计算成本。降低换手率比如用更长的预测周期或者加信号平滑。如果策略在扣除成本后没有超额收益说明它本身不成立。5. 从 demo 到可用策略验证、迭代与实盘前检查5.1 用 walk-forward 分析检验策略稳健性单次回测说明不了太多问题。我一般会做 walk-forward 分析把数据分成多个时间段每次用前一段训练、后一段测试滚动前进。如果策略在多个时间段都能保持正收益说明它有一定的稳健性如果只在某一两段表现好大概率是过拟合。def walk_forward(df, n_splits5, train_ratio0.7): n len(df) step n // n_splits results [] for i in range(n_splits): start i * step end min(start step, n) if end - start 100: continue segment df.iloc[start:end].copy() split_idx int(len(segment) * train_ratio) train segment.iloc[:split_idx] test segment.iloc[split_idx:] # 这里调用你的训练和回测函数 # model train_model(train) # perf backtest(model, test) # results.append(perf) return results这个框架的关键是每次只用历史数据训练用未来数据测试模拟实盘的信息约束。n_splits和train_ratio根据数据量调整数据少就少分几段数据多可以多分。5.2 实盘前必须检查的五个清单项第一信号执行延迟。确认回测里的成交价格和实盘一致信号至少延迟一根 K 线执行。第二手续费和滑点。按实际券商费率设置滑点至少按一个最小变动价位估算。第三仓位限制。单票最大仓位、总仓位上限、行业暴露限制这些在回测里就要加上。第四极端行情处理。涨跌停、停牌、流动性枯竭时回测怎么处理实盘就怎么处理。第五资金容量。策略能容纳多少资金超过容量后冲击成本会显著上升。小资金有效的策略大资金不一定有效。5.3 一个具体技巧用特征重要性做特征筛选训练完 LightGBM 后可以直接输出特征重要性把重要性低的特征去掉再训练。我一般会跑三轮第一轮用全部特征看重要性分布第二轮去掉重要性最低的 30%第三轮再去掉 20%。对比三轮的验证集表现选泛化最好的那一组。def select_features_by_importance(model, feature_cols, top_k20): importance model.feature_importance(importance_typegain) feat_imp pd.DataFrame({ feature: feature_cols, importance: importance }).sort_values(importance, ascendingFalse) selected feat_imp.head(top_k)[feature].tolist() return selected, feat_impimportance_typegain表示用信息增益衡量重要性比分裂次数更稳定。top_k根据样本量调整样本少就选少一点样本多可以多留一些。筛选后要重新训练和回测不能直接沿用旧模型。这套 demo 的代码我前后改了七八版最大的教训是不要相信任何没有扣除交易成本、没有做样本外验证的回测结果。机器学习在量化里能做的事是帮你从高维特征里找规律但它找出来的规律是不是真的需要你用严格的时间序列验证去判断。我自己的习惯是任何策略上线前至少跑三遍 walk-forward任何一遍表现异常就回去查数据。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询