news 2026/9/11 5:24:03

机器学习量化策略实战:backtrader多股回测与过拟合检验

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
机器学习量化策略实战:backtrader多股回测与过拟合检验

简介:面向金融量化入门者及Python开发者的机器学习量化投资实战项目,集数据获取、特征工程、LightGBM建模与历史回测于一体。项目内置10支股票样例,通过命令行即可完成从安装依赖到回测评估的完整流程,并输出累积收益、最大回撤、夏普率等核心指标,适合希望将机器学习落地到股票策略分析的读者快速上手。资源包共15个文件,包含5个Python源码程序、6个结果图表、依赖清单与使用说明文档,压缩包仅735KB,结构清晰便于二次开发。目前已吸引267人学习,源码提供完整回测框架和数据集管理逻辑,可作为量化策略研究的基础模板,也可扩展至更多股票与自定义特征,帮助降低入门门槛并提升策略验证效率。

1. 从机器学习到量化投资:为什么你的策略需要一套可回测的代码框架

把机器学习放进量化投资,第一直觉往往是让模型预测明天的涨跌方向。但真正决定策略能不能在真实市场里活下来的,不是某个模型有多新,而是从原始行情到回测报表这条链路是否干净。一个可复用的量化策略项目,通常由特征工程、模型训练、信号生成、回测引擎和结果报告五部分组成。源码的价值不在模型有多惊艳,而在特征是否统一、回测是否隔离未来信息。对量化和机器学习应用场景中的工程师来说,先读懂一套源码怎样把训练预测回测串成闭环,比追求单个指标重要得多。本文就沿着这个闭环,落到backtrader多股回测和过拟合验证上。

2. 从数据到交易信号:量化策略里的特征工程与机器学习模型选择

2.1 量化特征的最小集合:收益率、波动率与流动性的构造逻辑

特征工程这一步决定下限。很多人一上来就把能想到的技术指标全塞进去,RSI、MACD、布林带全部堆进一张表,模型训练完在训练集上表现很漂亮,一换时间段就崩。量价类数据的信噪比极低,机器学习算法碰到这种分布,第一诉求不是增加特征维度,而是降低噪声干扰。常见做法是先构造三个基础维度:收益率刻画收益的持续性,波动率刻画风险程度,成交量变化刻画流动性与资金参与度。特征生成之后先看分布、看缺失、看极端值,比直接跑模型更花时间。

数据来源方面,python爬虫和公开免费数据集都可以支撑这个环节,关键不在接口而在时间戳对齐。日线数据必须按交易日对齐,停牌日、节假日会导致DataFrame索引错位,特征计算的结果就会偏掉。这里给出一份最小可用的特征函数,后面的训练、预测、回测三个阶段共用这一份实现。

import pandas as pd import numpy as np def build_features(df: pd.DataFrame, lookback: int = 5) -> pd.DataFrame: df = df.copy() # 当前交易日收益率,后续波动率的计算基础 df['ret_1'] = df['close'].pct_change(1) # 多日累计对数收益率,压缩极端值带来的偏度 df['ret_lb'] = np.log(df['close'] / df['close'].shift(lookback)) # 滚动窗口波动率,反映短期风险水平 df['vol_lb'] = df['ret_1'].rolling(lookback).std() # 成交量与20日均量的比值,衡量资金活跃度 df['vol_ratio'] = df['volume'] / df['volume'].rolling(20).mean() return df.dropna()

pct_change(1)算的是单日涨跌幅,rolling(lookback).std()是过去5日收益率标准差,vol_ratio把成交量转化为相对水平而不是绝对量,这样不同股票的成交量差异不会主导模型。lookback参数是最值得做敏感性测试的对象,后面专门讲怎么验证它对回测结果的影响。

标签的构造与特征同等重要。二分类是量化项目里最稳妥的建模方式,标签定义为未来N日收益率的符号,make_label函数负责这件事。shift(-horizon)把未来收益平移到当前时间点,但会造成样本尾部出现NaN,dropna(subset=['label'])把这部分无效样本剔除。

def make_label(df: pd.DataFrame, horizon: int = 5) -> pd.DataFrame: # 未来horizon日的累计收益符号作为分类标签 df = df.copy() df['future_ret'] = df['close'].shift(-horizon) / df['close'] - 1 df['label'] = (df['future_ret'] > 0).astype(int) return df.dropna(subset=['label'])

horizonlookback一般保持同值,取3、5、8、13这类斐波那契数,便于后续敏感性分析时观察趋势。标签的正负样本比例很少是均衡的,牛市里正样本偏多,熊市里接近五五开,这个不均衡交给回测去检验,不要在训练前硬做重采样。

特征与标签必须放在同一个文件里维护

把特征函数和标签函数放在同一个独立模块,训练脚本、预测脚本、回测脚本都从该模块导入。不要在一个脚本里复制粘贴后再改条件,两处实现一旦不同,回测结果的解释力就消失了。项目维护中最常见的返工原因不是模型预测不准,而是训练和回测用了两套feature列。

注意:缺失值填充必须在划分训练集之前完成,否则填充统计量会泄漏到验证集,导致回测指标虚高。

2.2 标签构造与分类器选择:机器学习分类器在量化投资里的取舍

模型选择不需要复杂。逻辑回归、随机森林、LightGBM是三个典型的备选,分别代表线性、袋装树、提升树三类思路。量化特征分布噪声很大,用回归方式去预测具体价格通常误差离谱,更稳定的做法是把问题转换成分类。下表给出三者的差异:

模型对噪声容忍度训练速度过拟合风险适用数据量
逻辑回归极快可控中小
随机森林中等中,受树深影响大
LightGBM较高,依赖早停

随机森林在量价因子的非线性拟合上有天然优势,树深度设到5或6层,每个叶节点最少样本数设到50,过拟合空间被压得很小。LightGBM数据量大时优势明显,但必须配合早停机制,否则训练集表现一路走高而验证集不涨。机器学习实战里的调参经验放在量化场景依然有效,唯一的区别是时间序列不能随机打乱。

from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestClassifier feature_cols = ['ret_1', 'ret_lb', 'vol_lb', 'vol_ratio'] X = data[feature_cols] y = data['label'] # 按时间顺序切分,不做shuffle X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, shuffle=False ) model = RandomForestClassifier( n_estimators=200, max_depth=5, min_samples_leaf=50, random_state=42, ) model.fit(X_train, y_train) prob = model.predict_proba(X)[:, 1]

shuffle=False是金融时序与一般机器学习分类任务的根本差异,样本顺序承载了行情演化的信息,打散会掩盖过拟合的本质。min_samples_leaf=50相当于正则,把单个叶节点的决策限制得平滑。predict_proba给出的上涨概率稳定在0到1之间,作为后续信号的输入比直接使用预测类别要有用得多。

stratify参数在时序切分里不要开启,类别不均衡是市场常态,分层抽样会把不同时期的行情分布强行拉齐,让模型学到不存在的平稳性。

2.3 模型输出到交易信号的映射:阈值排序与仓位控制的实现

模型输出不能直接当信号。概率值需要经过阈值和排序两步加工才变成交易指令。阈值决定交易频率,属于策略层面的选择,要结合回测阶段的收益目标一起调。排序的意义在于,当候选股票超过持仓数量上限时,只保留概率最高的前N只。

threshold = 0.55 signal_series = pd.Series(prob, index=data.index) # 超过阈值的股票进入候选池,再按概率排序 candidate = signal_series[signal_series >= threshold].sort_values(ascending=False) # 每个交易日只保留概率最高的前5只股票 selected = candidate.groupby(candidate.index.date).head(5)

groupby(candidate.index.date)按日期分组,每个交易日只保留那一组里概率最高的五只股票,这样信号表直接和账户能承受的持仓数量挂钩。仓位权重方面,等权配置是最容易调试的起点,初始资金100万、单只股票10%目标仓位,剩余现金作为缓冲。如果改用市值加权,需要在信号表里额外传入每只股票的市值列,回测代码的复杂度会上升一个台阶,初期没必要。

3. backtrader 多股回测的关键实现:策略类编写与订单执行参数

3.1 backtrader策略类的固定骨架:初始化引用与next方法

回测引擎选backtrader,是因为它的多股数据接入和订单管理都比较成熟,开源社区资料也多。策略类需要继承bt.Strategy__init__中保存数据或指标引用,next在每一根bar上被调用,这就是典型的事件驱动式回测。代码里唯一要理解的抽象是:self.datas是一个列表,加载了几只股票就有几个数据对象。

import backtrader as bt class MLStrategy(bt.Strategy): params = ( ('signal_df', None), # 每日信号表,由训练阶段生成 ('target_pct', 0.1), # 单只股票目标资金占比 ) def __init__(self): self.signal = self.params.signal_df def next(self): # 当前bar对应的交易日 current_date = self.datas[0].datetime.date(0) for data in self.datas: symbol = data._name # 信号表里没有当天数据就跳过 if current_date not in self.signal.index: continue sig = self.signal.loc[current_date, symbol] pos_size = self.getposition(data).size if sig == 1 and pos_size == 0: # 开仓:目标仓位从0调整到target_pct self.order_target_percent(data, target=self.params.target_pct) elif sig == 0 and pos_size > 0: # 清仓:目标仓位归零 self.order_target_percent(data, target=0.0)

current_date通过self.datas[0]获取,回测框架要求所有数据按时间对齐,取第一个数据源的日期作为当前交易日是通用做法。self.signal.loc[current_date, symbol]要求信号表索引是日期、列名是股票代码,并且列名与data._name完全一致。

next方法内的订单状态判断

这段逻辑里最容易踩坑的是重复下单。next在每个交易日对每只股票执行一次,如果信号为1且持仓已经存在,再下一个目标仓位相同的订单虽然不会改变仓位,但会白白产生挂单和撤单流程,回测速度下降,费用计算也可能出错。所以开仓前用pos_size == 0判断当前无持仓,清仓前用pos_size > 0判断有持仓才执行。

order_target_percent会自动把仓位调整到指定比例,按账户权益的10%持仓,剩余资金留在现金账户,这比buy()固定股数的方式更贴近实际资金管理逻辑。唯一要留意的是目标仓位和手续费之间的相互作用,手续费会消耗一点现金,导致最终持仓比例与估值稍有偏差,这在回测精度要求不高的场景中可以接受。

提示:不要在连续多根bar里反复调用order_target_percent做微调,目标仓位的微小变化会放大换手费用,纯回测净值看似平滑,实盘成本早就把利润吃掉了。

3.2 多股数据接入与时间对齐:PandasData逐股加载方案

多股回测时,不同股票的上市时间、停牌规则、交易日期集并不一致。backtrader会把所有数据按时间对齐,缺失日期的股票在那一根bar上不触发next中的循环,但仍需注意订单买卖时价格获取的匹配。常见做法是先把每只股票整理成独立DataFrame,再逐股包装成PandasData对象加入Cerebro。

cerebro = bt.Cerebro() for symbol, df in stock_data.items(): # datetime列必须是DatetimeIndex或可解析的时间列 data = bt.feeds.PandasData(dataname=df, datetime='date') data._name = symbol cerebro.adddata(data) cerebro.addstrategy(MLStrategy, signal_df=signal_table) cerebro.broker.setcash(1000000.0)

_name属性是识别每只股票的钥匙,策略方法都用它来做信号查询和日志输出。如果DataFrame的列名与backtrader默认字段不一致,比如open列叫Open,必须在PandasData构造时显式映射,否则数据解析会静默失败,策略结果异常难排查。信号表本身也需要按股票代码整理成与_name相同的命名,否则self.signal.loc[current_date, symbol]会抛出KeyError。

多股策略的日志输出建议把data._namedata.datetime.date(0)一起打印,否则回测结束根本不知道哪只股票在什么时候触发过交易,也很难核对信号表和实际执行的偏差。

3.3 回测结果可信度参数表:手续费、滑点与最小交易单位

不设手续费和滑点的回测没有任何参考价值。手续费、滑点和最小交易单位三者共同决定了回测到实盘之间的成本鸿沟。下表给出一套日常项目可以复用的初始化参数:

参数推荐初始值调整策略
手续费率万2.5到万3按实际账户佣金乘2计双边成本
滑点0.1%到0.2%流动性差或资金量大时上调
最小交易单位100股A股按手取整,港股美股各按规则调
cerebro.broker.setcash(1000000.0) # 单边手续费万3,回测报告里要意识到这是单边 cerebro.broker.setcommission(commission=0.0003, stocklike=True) # 设置按成交额百分比的滑点 cerebro.broker.set_slippage_perc(perc=0.001)

setcommission的参数是按单边计算的,实际操作中买入卖出各收一次,综合成本要按双边看。set_slippage_perc会将市价订单的成交价格在开仓方向偏移0.1%,模拟市场冲击成本。如果发现回测对滑点变化非常敏感,比如从0.1%调到0.2%收益就由正转负,说明策略本身的毛利空间太薄,真实环境很难存活。

4. 源码工程结构与使用说明:把训练预测回测串成闭环

4.1 目录布局的分层设计与配置文件的唯一入口

一套能长期维护的量化源码,目录结构决定项目的可维护性。常见做法是将配置、数据、特征、模型、回测脚本分层放置,模块间的依赖方向保持单向。实际工程里,机器学习项目耗时最长的往往不是模型训练,而是数据组织和结果复盘,目录分层能做到什么程度,几乎决定了复盘效率。下面这个结构适合从几万元到几千万元资金的团队:

quant_project/ ├── config.yaml # 唯一的参数入口 ├── data/ # 原始行情数据,日线级别 ├── features.py # 特征和标签构造,唯一实现 ├── train_model.py # 训练模型并保存产出物 ├── predict_signals.py # 加载模型生成交易信号 ├── backtest.py # 回测入口,输出净值与指标 └── requirements.txt # 依赖清单

config.yaml是唯一的配置入口,训练和回测都从它读取参数,调整股票池或回测时间时不用翻代码;features.py是唯一特征实现,训练和回测都导入它,避免两套特征逻辑的偏差;train_model.pypredict_signals.py一个负责训练产出模型文件,一个负责预测产出信号表,职责互不重叠。这几个文件合在一起,就是完整项目的源码核心。

4.2 避免未来函数与数据泄漏:训练回测只用同一份特征代码

特征实现只有一个副本还不够,还要确保特征计算的时点语义正确。常见隐患是:当天的收盘价在收盘后才被知晓,如果用当天收盘价计算特征再去预测同一个交易日是否上涨,这就是典型的未来函数。严格处理方式是所有涉及价格的输出都至少向后平移一天。

# 规避方式:所有价格特征向后平移一天 df['ret_1'] = df['close'].pct_change(1).shift(1) df['vol_lb'] = df['ret_1'].rolling(5).std()

这种写法的结果是,当T日收盘后发出信号时,模型使用的全部是T日及之前已经确定的数据,信号真正生效在T+1日开盘或收盘。训练和回测都使用同一份移位后的特征矩阵,未来函数的风险就降得很低。数据泄漏的范围更广,除了时间顺序问题,还包括训练样本覆盖了验证时间段,所以切分必须严格按时间顺序进行。

4.3 从python环境安装到跑通回测的使用说明与常见问题

配置文件的最小写法

完整使用说明要覆盖环境准备、训练、信号、回测四个环节。环境准备部分建议使用虚拟环境,机器学习库之间依赖关系复杂,直接在全局环境安装容易冲突。

# config.yaml 最小配置 data_dir: ./data start_date: 2018-01-01 end_date: 2022-12-31 stock_pool: - 000001.SZ - 600519.SH lookback: 5 horizon: 5 threshold: 0.55 initial_cash: 1000000

lookbackhorizon在这里被显式配置,后续做参数敏感性测试时只需要改这两个值。start_date表示回测区间起点,训练集的起点通常要更早,在train_model.py内单独设置,两者不要混在一起。

python -m venv .venv source .venv/bin/activate pip install -r requirements.txt python train_model.py --config config.yaml python predict_signals.py --config config.yaml python backtest.py --config config.yaml

跑完train_model.py会在models目录生成模型文件,跑完predict_signals.py生成signals.csv,backtest.py读取这张表输出回测报告。遇到结果异常时优先检查config.yaml里的stock_pool是否与data目录下文件名一致,文件前缀不匹配是最常见的启动报错来源。

提示:Windows下激活命令改为.venv\Scripts\activate;用VSCode打开项目根目录后选择虚拟环境解释器,pyright能立即识别已安装的包,环境配置问题会少很多。

5. 回测过拟合的检验技巧与策略鲁棒性的进阶验证

5.1 参数敏感性分析判断过拟合边界

过拟合不会直接报错,它会在参数微调时暴露。验证时把核心参数按序列扫一遍,比如lookback设为3、5、8、13天,持仓数设为3、5、10只,观察夏普比率和最大回撤的变化趋势。每次扫描只动一个参数,其余条件固定,结果才有可比性。

for lookback in [3, 5, 8, 13]: feat = build_features(price_data, lookback=lookback) lab = make_label(feat, horizon=lookback) sharpe = run_backtest(feat, lab) print(f'lookback = {lookback}, sharpe = {sharpe:.2f}')

如果只有某个特定参数值效果好,两侧参数值收益断崖式下跌,这不是发现金矿,而是模型在记忆噪声。健康的参数区间应该表现为收益在一个较宽的范围内平稳变化,比如5到8天都能产出正收益,只是数值略有波动。敏感性分析结果建议直接以表格形式写进项目的README,这比任何口头解释都有说服力。

5.2 滚动窗口回测验证样本外稳定性

单次回测只能说明一个时间段。滚动窗口回测把数据切分成多个训练与验证段,前段训练模型,后段验证效果,然后整体前移重复运行,最后汇总各段样本外结果的复合收益。这样做能把市场风格切换、季节性波动都暴露在验证里。

for end_year in range(2019, 2023): train_end = f'{end_year}-01-01' train = full_data[full_data.index < train_end] val = full_data[(full_data.index >= train_end) & (full_data.index < f'{end_year + 1}-01-01')] oos_return = run_train_val(train, val) results.append(oos_return)

样本外收益如果每年都稳定为正,说明策略确实有持续性;如果只在某一两年赚钱,大概率是风格巧合而不是模型能力。做完滚动窗口后,把稳健的参数区间固定下来,再算一次完整样本的日内最高亏损和单笔最大回撤,这套结果就可以作为项目交付版本的依据。最终交付的回测脚本,把这套滚动验证的入口参数默认设成扫出来的稳定值,任何一台机器clone下来跑出来的报告都应该是同一份数字。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 5:23:00

Backstage 登录实战:从 GitHub OAuth 配置到登录验证与问题排查

Backstage 登录实战&#xff1a;从 GitHub OAuth 配置到登录验证与问题排查 【免费下载链接】backstage Backstage is an open framework for building developer portals 项目地址: https://gitcode.com/GitHub_Trending/ba/backstage 本篇技术指南以 docs/getting-sta…

作者头像 李华
网站建设 2026/9/11 5:22:19

AI原生SDLC操作手册:从需求到运维的六环节重塑

AI 原生 SDLC 操作手册&#xff08;The AI-Native SDLC playbook&#xff09;&#xff0c;这个标题背后其实藏着一个很现实的问题&#xff1a;当大模型已经能写代码、查 Bug、补测试的时候&#xff0c;我们原来那套软件研发流程到底还要不要&#xff1f;要的话&#xff0c;该怎…

作者头像 李华
网站建设 2026/9/11 5:16:55

AI Agent落地指南:市场需求、技术栈与实战避坑

1. 报告背景与市场情绪扫描1.1 从热搜词看需求侧的微妙转向这份报告的起因有点意思。我整理2026年8月的行业检索数据时发现&#xff0c;围绕“AI Agent”的关键词结构已经和两年前完全不同了。2024年大家搜的是“AI Agent是什么”“AI Agent和RPA有什么区别”&#xff0c;属于概…

作者头像 李华
网站建设 2026/9/11 5:11:24

LlamaIndex MboxReader 实战指南:从 mbox 邮箱文件到可检索文档

LlamaIndex MboxReader 实战指南&#xff1a;从 mbox 邮箱文件到可检索文档 【免费下载链接】llama_index LlamaIndex is the leading document agent and OCR platform 项目地址: https://gitcode.com/GitHub_Trending/ll/llama_index 导读 本指南围绕 LlamaIndex 仓库…

作者头像 李华
网站建设 2026/9/11 5:07:03

Agent持续进化:Hermes系统更新维护实战指南

做 Agent 的老朋友应该都有同感&#xff1a;第一次把 Hermes 部署起来、跑通第一个工具调用的时候是最爽的&#xff0c;之后真正磨人的反而是长期运行里的更新与维护。这个印象我特别深——项目刚上线那阵子&#xff0c;我一度以为 Agent 是一个“搭好就能一直跑”的东西&#…

作者头像 李华