ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

机器学习量化策略源码实战:从数据清洗到LightGBM与backtrader回测

机器学习量化策略源码实战:从数据清洗到LightGBM与backtrader回测 简介这是一份基于 Python 机器学习的量化投资策略项目源码完整覆盖数据下载、特征工程、模型训练与历史回测四大环节适合有一定编程基础、希望入门量化交易或金融建模的开发者学习和二次开发。压缩包共 15 个文件以 5 个 Python 脚本和 6 张可视化图表为主体另含依赖清单、股票列表、Markdown 说明文档及 gitignore 配置整体仅 735KB结构紧凑可快速对应数据、特征、模型、回测各模块。目前已有 267 人浏览学习。项目通过 tusare 获取股票历史行情可自定义股票列表来扩充数据源随后生成模型所需高级特征采用 LightGBM 训练策略模型。回测时会记录每日买入标的与收益并计算累积收益、最大回撤、夏普率等量化投资核心指标帮助使用者系统评估策略表现配合使用说明能够轻松复现从原始数据到回测结果的全流程并在调整股票池或参数的过程中加深对量化投研各环节的理解。1. 机器学习量化策略不是玄学这套源码回测到底能帮你走到哪一步很多刚接触机器学习的交易者拿到一份“量化投资策略项目源码”后的第一反应是把回测跑出来看一眼收益率然后盯着那条陡峭的净值曲线问这能直接上实盘吗我见过太多回测年化翻倍、实盘几个月就熄火的案例问题大多不在模型而在数据对齐、标签构造和回测设置上。这里面的核心链条——Python数据清洗、机器学习特征与模型训练、回测框架验证——恰恰是标题里源码、回测和使用说明三者要一起解决的。这套方案适合已经会写基础Python、想做机器学习选股但不想从零搭轮子的人也适合被股票预测模型坑过、想搞清楚“为什么回测和实盘差这么多”的人。先接受一个前提源码能跑通只是起点回测结果可信才是这个标题里真正值钱的另一半。2. 先把数据喂对特征工程与标签构造决定策略上限拿到这类源码包我建议第一件事不是运行 main.py而是先花半小时把使用说明或 README 读透确认三件事数据接口用的是什么、特征在哪个文件里生成、回测脚本的入口在哪。多数人翻车都是因为跳过了数据准备直接用别人处理好的特征跑模型出了问题根本不知道去哪查。2.1 数据源选型与清洗后复权、缺失值、涨跌停先处理常见做法是用免费或低成本的行情接口拉日线数据国内可选 akshare、baostock国外教程里常出现 yfinance。自己拉数据最需要注意的是复权方式。回测必须用后复权价格因为前复权会改变历史价格间的比例关系导致用历史数据训练的特征在回测时失真。下面是一段用 akshare 拉取某只股票日线并做基础清洗的示意代码具体接口参数以你装的 akshare 版本为准import akshare as ak import pandas as pd # 拉取股票日线adjusthfq 表示后复权 df ak.stock_zh_a_hist( symbol000001, perioddaily, start_date20150101, end_date20231231, adjusthfq ) # 统一列名后面特征和回测都靠这几个字段 df df.rename(columns{ 日期: date, 开盘: open, 收盘: close, 最高: high, 最低: low, 成交量: volume }) df df[[date, open, close, high, low, volume]].copy() df[date] pd.to_datetime(df[date]) df df.sort_values(date).reset_index(dropTrue) # 检查缺失和停牌日成交量为0的交易日直接过滤 df df.dropna() df df[df[volume] 0] print(df.head())这段代码的逻辑是先拿到后复权日线统一字段名再按时间排序并剔除停牌和异常空值。adjusthfq是后复权回测和特征计算都应该用它不要用未复权数据否则除权除息日会出现虚假的暴跌。停牌日通常成交量为 0 或直接缺失这类样本会影响特征计算的连续性过滤掉比填充更干净。另外提醒一句不同数据接口的复权字段差异不小有的接口默认前复权有的返回未复权。使用说明里如果写了数据来源就按它来如果没写自己务必确认一下复权口径。2.2 特征怎么构造动量、波动率与量价因子的一组可跑代码特征工程在量化里有一个特殊约束只能用当前时刻及之前的数据不能混进未来信息。常见的做法是把原始价格序列转成几类因子——动量类、波动类、量价类然后用滚动窗口计算。下面这段代码构造了 5 个基本面因子之外的高频量价特征# 基础收益率 df[ret] df[close].pct_change() # 动量因子过去5日、20日累计涨幅 df[mom5] df[close] / df[close].shift(5) - 1 df[mom20] df[close] / df[close].shift(20) - 1 # 波动率因子过去10日收益标准差 df[vol10] df[ret].rolling(10).std() # 量比因子当日成交量相对20日均量的放大程度 df[vol_ratio] df[volume] / df[volume].rolling(20).mean() # 乖离率收盘价相对20日均线的偏离 df[bias20] (df[close] - df[close].rolling(20).mean()) / df[close].rolling(20).mean() # 删除开头因为滚动窗口产生的NaN行 df df.dropna().reset_index(dropTrue)pct_change()计算的是相对前一天的收益率shift(5)取 5 天前的收盘价两者结合得到 5 日动量。rolling窗口会产生前 N 行为 NaN最后统一 dropna 是标准操作。这里的关键点是所有计算都只依赖过去的数据没有用到未来值。如果你跑的是多股票池还需要做截面标准化。纯时序因子在不同股票之间不可比比如平安银行的波动率和一只小盘股的波动率不在一个量级。常规做法是按日期分组在每个交易日内对因子做 z-score 或排名。这个细节决定了机器学习模型能不能真正学到“横截面选股”规律。2.3 标签构造与按时间切分别把未来数据装进训练集标签构造是这类项目里最容易被忽略但最影响结果的一步。常见的做法是用未来 N 日收益率构造分类标签比如未来 5 日收益超过 2% 记为 1否则为 0。但这里有一个经典的坑用shift(-5)取未来数据时最后 5 行会得到 NaN很多人不处理直接丢给模型训练时没报错预测时却可能出现莫名其妙的结果。更严重的是如果切分训练集和验证集时用了随机抽样同一段行情会同时出现在训练集和验证集里相当于透题。# 未来5日收益作为标签 df[future_ret] df[close].shift(-5) / df[close] - 1 df[label] (df[future_ret] 0.02).astype(int) # 删除末尾5行NaN标签 df df.dropna().reset_index(dropTrue) # 按时间顺序切分不能用随机切分 cutoff1 int(len(df) * 0.7) cutoff2 int(len(df) * 0.85) train df.iloc[:cutoff1] valid df.iloc[cutoff1:cutoff2] test df.iloc[cutoff2:]这段代码的标签逻辑是未来 5 日涨幅超过 2% 才算正样本。2% 这个阈值可以根据标的不同调整指数和个股差异很大。切分比例我一般用 7:1.5:1.5或者更保守的 6:2:2。注意dropna()必须在切分前完成否则标签泄露到训练数据里。除了时间顺序切分还有一个常被忽略的点验证集和测试集之间要留一个间隔避免紧挨着的行情数据产生时序相关性。比如训练集到 2022 年 6 月验证集从 2022 年 9 月开始留出两三个月的缓冲评估结果会更接近真实部署表现。3. 用LightGBM训练选股模型从基线到可复现的调参流程在机器学习选股这个方向模型选型最稳妥的不是深度学习而是梯度提升树模型尤其是 LightGBM。这个判断来自它的实际表现和数据特性表格型特征加非线性关系是它的主场。3.1 表格型特征为什么先选LightGBM而不是LSTM量化投资里的输入特征大多是表格形态每行是一个股票在某一天的因子值列是动量、波动率、估值等。这类数据有两个特点特征之间可能有复杂交互但样本量远小于自然语言或图像同时噪声极高股价短期走势很大程度受市场情绪和资金面影响模型很容易过拟合。LightGBM 对这类表格数据有天然优势训练快、自带正则化、能处理缺失值、特征重要性可以直接输出方便你做因子筛选和策略解释。LSTM 等序列模型适合的是长程依赖明显的任务而日频选股特征并不具备强序列依赖。从性价比角度看先从 LightGBM 起步跑通全流程后再考虑深度学习也不迟。3.2 一份能直接跑的LightGBM训练与验证脚本特征是上一章构造的量价因子标签是二分类。训练时我用早停避免过拟合并把验证集指标打印出来import lightgbm as lgb from sklearn.metrics import roc_auc_score features [mom5, mom20, vol10, vol_ratio, bias20] X_train train[features] y_train train[label] X_valid valid[features] y_valid valid[label] train_set lgb.Dataset(X_train, y_train) valid_set lgb.Dataset(X_valid, y_valid, referencetrain_set) params { objective: binary, metric: auc, learning_rate: 0.02, num_leaves: 31, feature_fraction: 0.7, bagging_fraction: 0.8, bagging_freq: 1, verbose: -1 } model lgb.train( params, train_set, num_boost_round2000, valid_sets[valid_set], callbacks[lgb.early_stopping(100), lgb.log_evaluation(100)] ) valid_pred model.predict(X_valid) print(验证集AUC:, roc_auc_score(y_valid, valid_pred))这里几个参数值得细说。learning_rate0.02是学习率越小越不容易过拟合但要配更多的迭代轮数num_leaves31控制树的复杂度叶子数越大模型越容易记住噪声feature_fraction0.7表示每棵树只用 70% 的特征相当于特征层面的随机性能抑制过拟合bagging_fraction0.8是行采样每棵树只用 80% 的样本。early_stopping(100)的意思是验证集 AUC 连续 100 轮不提升就停止这个数值要和学习率配合学习率越低早停的耐心轮数应越大。3.3 调参三板斧学习率、num_leaves与早停调参最忌一上来就 grid search耗时且容易过拟合。我的顺序一般是先固定学习率为 0.01 到 0.03通过早停确定大概的迭代轮数然后调num_leaves从 16 试到 64观察验证集 AUC 的变化。num_leaves越大训练集 AUC 越高但验证集 AUC 可能开始回落这个拐点就是合适值。最后微调feature_fraction和bagging_fraction一般 0.6 到 0.9 之间。还有一个容易忽略的参数是min_data_in_leaf默认 20对股票这种噪声大的数据我常调到 200 以上强制每个叶子至少有几百个样本能显著减少极端预测值回测资金曲线更平顺。评估模型不止看 AUC。量化里更常用 IC信息系数即预测分数与未来收益的秩相关系数一般要求 IC 均值在 0.03 以上才值得继续。可以把验证集预测结果按分数分 10 组看最高组的平均收益是否明显高于最低组这个分组的单调性是判断策略是否有真实预测力的重要依据。如果分组收益没有单调性就算 AUC 再高回测大概率也是过拟合的产物。4. 用backtrader把预测变成净值曲线单股与多股回测配置模型训练完下一步是把预测分数接进回测框架。backtrader 是目前 Python 生态里用得最多的开源回测框架之一支持多股回测、自定义手续费和滑点对这份源码项目来说足够用。这里最关键的思路是回测脚本里的交易信号必须依赖预测分数而预测分数必须由训练好的模型在“当时”生成不能提前算出整段历史的预测值再喂给回测。4.1 从预测分数到交易信号阈值、仓位与换手控制模型输出的是一个 0 到 1 的预测概率不能直接当信号。我一般先看训练集预测分数的分位数分布比如取 70% 分位数作为买入阈值分数高于阈值才买入低于 40% 分位数时清仓。这样做的目的是控制交易频率避免模型概率一波动就频繁进出手续费会吃掉利润。回测里常见的换手率控制在月度 30% 到 100% 之间比较合理过高说明信号不稳定。4.2 backtrader最小回测工程手续费、滑点与资金设置下面是一个最小可用的 backtrader 策略类策略只在预测分数超过阈值时买入低于另一个阈值时卖出import backtrader as bt class MLStrategy(bt.Strategy): params ( (buy_th, 0.6), (sell_th, 0.4) ) def __init__(self): # 假设数据馈送里已有一列score由模型预测生成 self.score self.datas[0].score def next(self): if not self.position and self.score[0] self.p.buy_th: self.buy(size100) elif self.position and self.score[0] self.p.sell_th: self.close() cerebro bt.Cerebro() # 假设data是已经按日期排序好的数据馈送 cerebro.adddata(data) cerebro.addstrategy(MLStrategy) cerebro.broker.setcash(100000.0) cerebro.broker.setcommission(commission0.0003) cerebro.addanalyzer(bt.analyzers.Returns, _namereturns) results cerebro.run() print(期末总资金:, cerebro.broker.getvalue())buy_th和sell_th是买卖阈值size100是每次买入股数。setcommission(commission0.0003)设置的是单边万三的手续费这已经是比较理想化的费率。真正实盘还有印花税和滑点回测至少要再加滑点backtrader 里通过cerebro.broker.set_slippage_perc(perc0.001)设置按成交价的千一滑点。很多源码包的回测脚本默认没有滑点跑出来的收益会虚高使用说明里如果没提建议自己加上。4.3 backtrader多股回测多标的同时交易时的资金分配多股回测是另一个高频需求热搜里的 backtrader 多股回测关注度很高。backtrader 支持向 Cerebro 添加多个 data feed策略通过self.datas访问所有标的。常见做法是每个交易日对所有股票打分选出分数最高的前 N 只持有等权重分配资金class MultiStockStrategy(bt.Strategy): def __init__(self): self.datas_len len(self.datas) def next(self): # 收集当日所有标的的预测分数 scores [(i, self.datas[i].score[0]) for i in range(self.datas_len) if not self.datas[i].position] scores.sort(keylambda x: x[1], reverseTrue) # 买入前3名 for idx, score in scores[:3]: cash_per_stock self.broker.getvalue() / 10 size int(cash_per_stock / self.datas[idx].close[0]) self.buy(dataself.datas[idx], sizesize)这段代码里self.datas[i]是第 i 只股票的数据流score[0]表示当前 bar 的预测值。买入前 3 名股票且控制单只仓位为总资金的 10%是为了分散风险。多股回测的坑在于资金分配和停牌处理如果某只股票不在交易日或涨停买不进backtrader 默认按下一根 bar 成交这个假设在实盘中不成立。处理办法是回测前把停牌日和涨跌停日的预测分数置为无效。5. 回测避坑指南结果漂亮却实盘翻车的4个根源回测曲线漂亮而实盘亏损是量化项目里最常见的翻车模式。下面这几个坑是这类源码项目里反复出现的每条都按现象、原因、解决的顺序说清楚。5.1 未来函数净值曲线在预测发布日跳涨现象回测净值曲线在某个日期突然跳升然后在下一个交易日又开始正常波动。原因预测分数在计算时使用了当日收盘后才知道的数据但信号却在当天开盘时就执行了交易交易时间点比数据可用时间更早。解决将信号推迟一个交易日。backtrader 里可以通过self.datas[0].close[0]拿到当天已确认的收盘价但买入动作要在下一个 bar 开盘执行也就是用next()里的self.datas[0].close[-1]生成信号并buy()确保信号不会偷看未来。5.2 幸存者偏差与涨跌停买不进现象回测收益里包含了很多如今已经退市或表现极差的股票并且在上涨时总能以接近开盘价买入。原因选股池用的是当前仍然上市的股票过去已经退市的股票被过滤了这就是幸存者偏差。同时回测默认股票在涨停板也能买入实际根本买不进。解决选股池按历史快照的成分股构造至少也要在回测脚本里做流动性过滤把当日涨停或成交额过低的股票排除在可买列表外。5.3 过拟合在同一段测试集上反复调参现象验证集 AUC 很高但换了新的时间段后表现断崖式下跌。原因在同一个测试集上反复调整参数模型和调参者一起记住了这段行情的噪声。这不是模型的问题是评估流程的问题。解决训练集、验证集、测试集严格分离测试集只能在最终阶段使用一次。更稳妥的做法是滚动训练见下一章的 walk-forward 流程每个时间段都重新训练再验证。5.4 手续费与滑点设成0现象回测年化收益很高但交易次数也异常多程序化实盘后收益大幅缩水。原因回测把交易成本设为零高频交易产生的摩擦成本完全没有体现。A 股双边手续费加印花税、滑点合计约千二到千三高频策略可能更高。解决回测参数至少设置双边千二交易成本和一波滑点然后再看收益。用 backtrader 的话setcommission(commission0.0003)只是单边佣金还需要在评估时把滑点和印花税估算进去或者直接调高佣金比例模拟总成本。6. 让策略经得起检验Walk-forward滚动训练与绩效指标验收6.1 Walk-forward滚动训练模拟真实部署节奏回测里最容易骗自己的方式是拿 2015 到 2023 全量数据训练然后在同一段时间内“验证”。实盘不是这样运行的模型需要不断重新训练。常见的做法是 walk-forward每 6 个月用过去 3 年数据重新训练一次模型然后只对下一个月的行情做预测和回测逐段滚动。这个过程可以用时间序列交叉验证实现from sklearn.model_selection import TimeSeriesSplit tscv TimeSeriesSplit(n_splits5) for train_idx, test_idx in tscv.split(X): X_train_fold X.iloc[train_idx] y_train_fold y.iloc[train_idx] X_test_fold X.iloc[test_idx] y_test_fold y.iloc[test_idx] # 每一折都重新训练LightGBM并记录绩效TimeSeriesSplit按时间递增切分确保测试数据永远在训练数据之后。每一折的模型绩效分别记录最后看平均表现是否稳定而不是盯着某一折的高收益。这个方法会显著拉长回测时间但它是判断策略是否值得投入的最可靠手段。6.2 绩效指标怎么读年化、回撤与夏普之外的三张表最后验收策略时不要只看年化收益率和夏普比率我一般会额外看三个指标最大回撤、胜率、盈亏比。最大回撤超过 20% 的策略需要慎重配置仓位因为实盘回撤会影响执行纪律胜率低于 35% 但盈亏比高的策略可以接受但必须确认连续亏损次数在可承受范围内。把回测脚本输出的每笔交易记录导出逐年统计表现观察是否集中在某一年赚钱。如果收益全部来自单一年份的特定行情那这个策略在下一个市场环境下大概率失效。我自己做这类项目有个习惯回测结果保存后隔一周再回来看一遍如果还是觉得值得投才做小资金实盘验证。量化投资里最贵的不是模型训练时间而是对回测结果的过度信任。希望这些从数据准备到回测验收的细节能帮你在拿到源码后少走一段弯路。提示以上所有代码和参数基于常规开源工具链实际使用时请以你本地环境和源码包内使用说明为准。本文还有配套的精品资源点击获取
返回列表