ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python量化交易回测系统源码拆解:从ETF轮动到随机森林策略

Python量化交易回测系统源码拆解:从ETF轮动到随机森林策略 简介这份基于 Python 的量化回测平台系统源码包面向有编程基础、希望用日线数据验证交易策略的量化初学者与金融开发者。资源采用模块化设计涵盖策略编写、回测执行、数据导入与结果展示内置 ETF 轮动、量价双金叉、基于随机森林三套可运行策略可直接修改初始参数后运行也能参照说明编写自定义策略适合快速上手本地量化回测。压缩包共 17 个文件总大小 10.43MB以程序代码、行情表格、分卷数据包及说明文档为主并附回测结果图与演示工程便于对照学习与复用。下载后可获得完整源码、示例行情数据、结果图片及中英文说明按说明文档指引放置数据并配置回测模块即可复现三套策略效果并在此基础上迭代个人策略。目前已有 437 人学习内容紧凑、开箱即用适合作为量化回测入门与二次开发的实用起点。1. 日线级别策略回测为什么要自己搭一套Python系统很多人会在网上找现成的量化平台但真正跑起来会发现两个尴尬的现实一是数据导出和复权规则绑定得很死换个数据源就要重写一遍二是策略库里全是“演示级”代码回测结果好看模拟盘一上就变形。这个基于Python的量化回测平台系统源码好就好在它把回测框架、交易模块、数据文件全部拆开放在你面前。你能直接编辑策略、改参数甚至把回测引擎单独拿出来接自己的行情数据。它内置的ETF轮动策略跑一轮只需要几秒而量价双金叉和随机森林策略大约需要十分钟这给“先验证想法、再追求效率”的用户留足了空间。适合已经有Python基础想从数据处理跨入量化交易的人。它不是那种一键生成的玩具而是一个能让你逐行搞懂回测逻辑的底稿。2. 拆解源码结构数据、策略、回测引擎如何分头协作2.1 解压后的文件职责一览拿到压缩包后你会看到一组以quant-strategy-master为根目录的文件。命名并不完全规范但逻辑是清楚的。我把关键文件按职责分类你可以对照自己电脑里的解压结果看文件/目录职责说明main.py回测入口负责读取参数、调用回测模块和交易模块通常是你运行的第一个文件strategy.py策略定义内含ETF轮动、量价双金叉、随机森林三个策略的类或函数porfolio.py投资组合模块根据策略信号计算每日持仓权重、市值、交易记录trade_after.py交易后处理处理手续费、滑点、交易信号落地生成最终成交明细find.py/history.py/output.py辅助工具分别负责数据寻址、历史数据处理、结果图表输出000001sh.xlsx指数日线数据上证指数日K通常作为基准或回测标的python_etf_6只.xlsxETF日线数据6只ETF的日K用于ETF轮动策略README.md/README.en.md使用说明英文说明也可用中文readme里会写初始参数怎么调quant-project-展示结果用.ipynb结果展示Jupyter Notebook方便你直接看回测曲线和指标Python_2020.part1.rar/part2.rar分卷压缩包大概率是依赖模块或补充数据需要解压后放到运行路径下这里要说一个容易被忽略的点Python_2020.part1.rar和part2.rar是两个分卷必须下载完整后再解压。很多人只解压了第一个文件后面运行报ModuleNotFoundError其实就是依赖模块没放进来。我一般会把所有rar文件放到同一目录先执行unrar x Python_2020.part1.rar让它自动带出part2内容。2.2 Excel日线数据导入pandas怎么读才不会踩坑项目里的数据文件是.xlsx格式这个比较少见——很多回测框架都支持csv但Excel文件更容易被无经验用户直接用Excel打开检查。数据导入这一步常见做法是用pandas.read_excel但要注意日期列和时间列的处理。下面是我会写的读取函数import pandas as pd def load_daily_data(path, date_coldate, price_colclose): df pd.read_excel(path) df[date_col] pd.to_datetime(df[date_col]) df.set_index(date_col, inplaceTrue) df df.sort_index() # 保留必要的价格列避免后面重复取数 df df[[price_col]] df.columns [close] return df.dropna()这个函数的重点是先解析日期列再按日期排序。read_excel返回的DataFrame行顺序往往是乱序的如果不sort_index()策略内部用前复权价格计算涨跌幅时会出现时间倒流信号全部错乱。dropna()则保证缺失K线不进入回测否则乘出来的收益会出现nan导致整个净值曲线断掉。当你把000001sh.xlsx和python_etf_6只.xlsx读取之后最好统一一下索引格式。比如两只文件的日期一个带时分秒一个不带合并时就会出问题。我一般在导入后加一行df.index df.index.normalize()把时间归一到交易日零点这一步能省掉后面很多对齐的麻烦。2.3 初始参数回测模块的起点都在main.py里回测系统的初始参数包括初始资金、手续费率、滑点、回测起止日期这些字段通常集中在main.py顶部或者单独一个config区域。下面是我整理出的核心参数结构# main.py 示例参数 INIT_CAPITAL 1_000_000 # 初始资金单位元 COMMISSION_RATE 0.0003 # 单边手续费率 SLIPPAGE_RATE 0.001 # 滑点率按价格比例计算 START_DATE 2018-01-01 # 回测起始日期 END_DATE 2023-12-31 # 回测结束日期 BENCHMARK 000001sh # 基准指数用于计算超额收益 DATA_FILE python_etf_6只.xlsx # 回测标的的数据文件注意这里的滑点率SLIPPAGE_RATE含义是每次买入或卖出时成交价格在信号的收盘价基础上再偏移0.1%。这个值在ETF上一般设成0.0005到0.002之间如果设成0回测收益会明显虚高。手续费率COMMISSION_RATE对于ETF通常是万2.5到万3包括佣金但不包括印花税——因为ETF本身不收取印花税。你可以看到项目设计者已经把“股票和ETF的差异”考虑进去了因为压缩包里给出的数据文件都是ETF日线所以默认参数更适合ETF场景。复制main.py到你的运行路径后第一件事不是直接运行而应该打开文件确认数据路径。DATA_FILE默认写的是python_etf_6只.xlsx如果你的文件名字带中文或空格pandas很可能因为编码问题读取失败。我就遇到过Windows系统下中文文件名导致unicode error的情况。最简单的办法就把文件重命名为纯英文与DATA_FILE保持一致。3. 三个内置策略从ETF轮动到随机森林代码与参数逐行拆解3.1 ETF轮动策略动量因子的极简实现这个策略跑得快因为它本质上是每个月或每周重新选一次持仓不需要逐小时计算指标。核心逻辑是计算每只ETF在过去N个交易日的涨幅选择涨幅最高的那只或几只买入其余资金空仓。这种动量因子在多资产ETF中效果通常不错因为ETF跨行业强趋势会持续一段时间。我在strategy.py里看到类似下面的代码结构import pandas as pd import numpy as np def etf_momentum_strategy(df_dict, lookback20, top_k1): # df_dict: 每只ETF的DataFrame键是ETF代码或名称 rets {} for code, df in df_dict.items(): rets[code] df[close].pct_change(lookback) # 按最后一天动量排序 last_momentum pd.DataFrame(rets).sort_index().tail(1).T selected last_momentum.nlargest(top_k, 0).index.tolist() return selectedlookback20表示过去20个交易日涨幅约等于一个月。top_k1表示只持有动量最强的那一只ETF。这里有个细节pct_change(lookback)计算的是close[t] / close[t-lookback] - 1但策略是在最后一个交易日收盘后产生信号实际买入发生在下一个交易日。如果项目代码里没有做这一步滞后就会出现“用当天收益率决策、再用当天收益结算”的未来函数回测结果虚高。我检查过的版本里通常会在交易模块中把信号整体shift(1)没有的话需要你自己补上。为什么要设置top_k1而不是多选几只轮动策略回测结果越好通常意味着持仓越集中。持仓一只ETF波动一定会比持有三只大但换手率低、逻辑清晰。如果设置top_k2资金等权分配给两只回撤更小但可能出现两只ETF相关性很高跟持有一只差别不大。判断策略选型时你要看的是“轮动逻辑是否真实存在”比如成长ETF和红利ETF轮动相关性低效果就会好于两只同类宽基。3.2 量价双金叉策略均线与成交量的叠加确认量价双金叉策略运行约十分钟因为它可能对每一只股票/ETF都计算了全历史区间的均线和成交量均线。这里的“双金叉”指的是收盘价均线金叉以及成交量均线同时金叉两条信号叠加才触发买入。这样做的好处是过滤掉无量上涨的假突破。示意代码如下def volume_price_golden_cross(hist_df, fast5, slow20, vol_fast5, vol_slow20): df hist_df.copy() df[ma_fast] df[close].rolling(fast).mean() df[ma_slow] df[close].rolling(slow).mean() df[vol_ma_fast] df[volume].rolling(vol_fast).mean() df[vol_ma_slow] df[volume].rolling(vol_slow).mean() df[price_cross] (df[ma_fast] df[ma_slow]).astype(int) df[vol_cross] (df[vol_ma_fast] df[vol_ma_slow]).astype(int) buy_signal (df[price_cross] 1) (df[vol_cross] 1) \ (df[price_cross].shift(1) 0) return buy_signal这里的fast5和slow20是常见的双均线组合。vol_fast5是5日成交量均线vol_slow20是20日成交量均线。买点条件是价格均线从下穿转为上穿同时成交量均线也已经处于多头排列。注意最后一行使用了price_cross.shift(1) 0这表示“当前一根K线快线还在慢线之下现在变成了之上”也就是金叉发生的那一根k线。如果没有shift(1)你会把每一个“快慢线都在多头排列”的日子都当作买入点导致信号泛滥。这个策略运行慢的另一个原因是它可能对python_etf_6只.xlsx里的每一只ETF都做了循环回测每次循环内部都要计算rolling均值。但十分钟的耗时更可能来自结果可视化而不是策略计算本身。如果是用matplotlib逐日绘制资金曲线那这个时间很正常。建议你第一次运行前先删掉绘图部分只输出绩效指标数组耗时可以降到一分钟以内。3.3 基于随机森林的策略机器学习如何避免“偷看未来”第三个策略把sklearn的随机森林引入回测。典型做法是构造特征矩阵例如过去5日收益率、过去10日波动率、成交量变化率、MACD柱状图等然后以未来N日收益是否为正作为标签训练一个二分类器。每周或每月重新训练一次预测下一个周期上涨概率概率超过阈值的标的就买入。下面是常见的简化版代码from sklearn.ensemble import RandomForestClassifier def random_forest_signal(df_dict, feature_cols, n_estimators200): all_df [] for code, df in df_dict.items(): tmp df.copy() tmp[ret_5] tmp[close].pct_change(5) tmp[vol_std_10] tmp[close].pct_change().rolling(10).std() tmp[code] code all_df.append(tmp) data pd.concat(all_df).dropna() # 未来5日收益为正作为标签 data[label] (data[close].shift(-5) data[close]).astype(int) # 只用历史数据训练避免未来函数 split_idx int(len(data) * 0.8) train data.iloc[:split_idx] test data.iloc[split_idx:] model RandomForestClassifier(n_estimatorsn_estimators, random_state42) model.fit(train[feature_cols], train[label]) test[prob] model.predict_proba(test[feature_cols])[:, 1] return test这里最大的坑是shift(-5)生成了未来标签这个标签只在训练阶段使用没问题。但如果你直接用全体数据训练后再回测那测试集也参与训练会导致分类器“记住”了未来走势。执行到测试集回测时你用的其实是已经看过全样本的模型结果会离谱地好。所以必须像这里一样按时间顺序切分训练集和测试集。n_estimators200对日线数据通常够用。特征数量不多时200棵决策树已经能稳定概率估计。如果你想加快速度可以降到100如果某次训练报ConvergenceWarning大多是特征里有nan这时回看dropna()是否执行成功。这个策略运行十分钟主要原因就是每次训练会构造大量滞后特征加上随机森林本身在大样本上比较慢。如果你自己写策略可以先用ETF轮动把流程跑通再上模型。3.4 三个策略如何选运行时间与适用场景对比策略运行时间换手频率依赖模块适合场景ETF轮动秒级低月度调仓pandas, numpy快速验证趋势跟踪想法量价双金叉约10分钟中信号出现时pandas, matplotlib研究均线系统的交易纪律随机森林约10分钟中高模型周度训练pandas, scikit-learn尝试用机器学习扩展特征维度运行时间对选择策略非常重要。我建议把ETF轮动作为“默认基线”因为它足够快能帮你排查main.py到porfolio.py之间的问题。如果你发现随机森林策略报内存错误别再调大n_estimators先检查数据量是否过大——6只ETF日线数据拉长到十年也不过几万行内存占用不会太高。真正原因是重复拼接DataFrame时产生了索引累计建议在每轮循环后data pd.concat(list).reset_index(dropTrue)。4. 在本地复现Python环境搭建、运行回测与结果解读4.1 从Python安装到vscode环境配置这个项目没有复杂的依赖核心模块是pandas、numpy、matplotlib、scikit-learn、openpyxl用于读取xlsx。首先确认你本地已经安装Python 3.8及以上版本。如果你还在用系统自带旧版建议从官网下载最新稳定版安装时勾选“Add Python to PATH”否则后面命令行里输入python会提示找不到。我推荐直接在VSCode里新建一个虚拟环境避免把基础环境搞乱python -m venv venv # Windows激活虚拟环境 venv\Scripts\activate # macOS / Linux source venv/bin/activate pip install pandas numpy matplotlib scikit-learn openpyxl这里逐条解释python -m venv venv创建隔离的虚拟环境避免你以后某个项目需要更高版本numpy时相互覆盖。激活后所有pip install都装进这个环境。最后一行一次性安装了6个库。如果网络比较慢可以添加-i https://pypi.tuna.tsinghua.edu.cn/simple换国内镜像源这一点在vscode python环境配置时非常常见。装完后验证版本python -c import pandas, sklearn, openpyxl; print(ok)如果报ModuleNotFoundError: No module named openpyxl说明你的pandas不能读取Excel文件必须重装openpyxl。这个错很容易被忽略因为pandas对csv是无缝支持但xlsx需要额外引擎。4.2 把数据放到正确的位置并运行把解压出来的000001sh.xlsx、python_etf_6只.xlsx放到与main.py相同的目录下。如果压缩包里的数据文件在data/子目录则需要先改main.py里的路径。然后打开strategy.py确认当前激活的是哪个策略。我见过的写法是在main.py里有一个变量控制# main.py 中策略开关按需取消注释 # from strategy import etf_momentum_strategy # from strategy import volume_price_golden_cross from strategy import random_forest_strategy这个项目为了让用户快速跑通可能会在strategy.py里把三个策略全部定义好但只会执行其中一个。我的建议是第一次运行先启用ETF轮动策略并减少回测区间比如把START_DATE改成2022-01-01这样验证流程更快。在终端执行python main.py如果看到Strategy backtest started...之类的输出说明进入了正常流程。运行过程中会生成多个结果图比如ETF轮动策略回测结果.png、量价双金叉策略结果图.png这些图实际是回测完成后保存下来的供你或不方便运行代码的同事在浏览器里查看。如果提示FileNotFoundError检查文件路径是否包含中文因为某些Python版本在Windows上打开中文路径时编码不一致解决办法是把整个项目文件夹移动到纯英文路径下比如D:\quant-backtest。还有一个高频报错是ValueError: cannot reindex from a duplicate axis。这通常出现在多只ETF数据拼接时某一只ETF的索引里出现了重复日期比如停牌复牌后重复K线。用df.index.is_unique检查一下如果不唯一执行df df[~df.index.duplicated(keeplast)]去重即可。4.3 看懂回测结果收益率、回撤与超额收益项目里用output.py和history.py生成结果最终展示在quant-project-展示结果用.ipynb中。典型的回测输出包含累计收益率年化收益率最大回撤夏普比率与基准000001sh的对比我一般会在output.py里增加一个简单函数输出关键指标def report_metrics(nav, benchmarkNone, freq252): total_return nav.iloc[-1] / nav.iloc[0] - 1 years len(nav) / freq annual_return (nav.iloc[-1] / nav.iloc[0]) ** (1 / years) - 1 rolling_max nav.cummax() drawdown nav / rolling_max - 1 max_drawdown drawdown.min() return { total_return: total_return, annual_return: annual_return, max_drawdown: max_drawdown, }这段代码里nav是净值序列freq252表示一年按252个交易日计算。最大回撤用cummax求历史最高点净值然后计算当前净值相对前高的回撤取最小值就是最大回撤。注意最大回撤的负值表示亏损幅度比如-0.23代表最大回撤23%。如果项目绘制的回撤图是正数通常展示的是绝对值。回测结果不是只看收益率。一个年化30%但最大回撤40%的策略实盘根本拿不住。我曾经调过一个轮动策略把top_k从1改成3后年化从35%降到22%但最大回撤从40%降到15%夏普比率反而从0.8升到1.4。所以如果你看到项目示例里的ETF轮动策略回测结果.png净值曲线很高一定要先看最大回撤数字不要被总收益迷惑。历史数据文件里的000001sh.xlsx是上证指数项目把它默认当作基准。如果你回测的是ETF轮动更好的基准应该是中证500指数或沪深300指数但项目里没有提供额外文件。你可以从开源数据库里导出对应指数日线数据替换成自己的.xlsx文件然后在main.py里修改BENCHMARK指向新文件。注意同样是xlsx格式列名要统一成date和close否则读不到数据。5. 自己写策略前必须先绕过的四个坑与一个验证技巧写自定义策略听起来很自由但项目框架里strategy.py、porfolio.py、trade_after.py之间的接口是三段式的策略信号 → 组合权重 → 交易撮合。你只需要改strategy.py的输出格式即可。四个常见的坑我逐个说明。第一个坑是未来函数。判断方式很简单t时刻产生的信号是否只用了t时刻及之前的数据。如果代码里用到close[t1]或者label.shift(-1)参与信号生成那一定是未来函数。我会在写完策略后把close整个shift一天再跑一遍如果收益几乎不变说明策略可能已经包含未来信息——因为真正有效的策略信号晚一天触发结果应当明显变差而不是不变。第二个坑是数据对齐。当你用6只ETF做轮动时某只ETF停牌一天它的pct_change会出现nan。如果直接用nlargest选最强停牌的ETF会被算作跌停导致策略空仓。正确做法是先用fillna(0)填充停牌日的收益或者在计算动量时要求至少有一段连续交易日。第三个坑是训练集和测试集混在一起。随机森林策略尤其容易犯我在前面代码里已经做了时间切分。如果你在自己的策略里引入了机器学习模型请务必做滚动训练——比如每20个交易日重新训练一次而不是只做一次80/20切分。一次切分会高估实盘表现因为实盘是将来发生的你无法用未来的半段数据做一次固定的训练。第四个坑是输出格式与回测模块不匹配。porfolio.py通常希望strategy.py返回一个日期为索引、标的代码为列名的DataFrame其中1代表持有0代表空仓。如果你返回的是一串买点信号交易模块就无法计算每日权重。建议参照已有的etf_momentum_strategy返回值把自定义策略的输出也整理成相同格式。最后给你一个具体技巧在trade_after.py里加一段“信号延迟触发”的断言强制把信号推迟到下一根K线开盘时执行。实现是在信号DataFrame上调用.shift(1)然后对比延迟前后绩效差异。比如下面的代码def delayed_signal(signal_df, shift1): return signal_df.shift(shift).fillna(0)将延迟后的信号传入porfolio.py如果总收益相比原信号下降超过30%说明原策略里可能掺入了当天收盘信息。这个方法比人工审计代码快得多。我自己每次写完新策略都会跑一次延迟对比确认收益衰减在合理范围内。这样你在基于这套Python量化回测平台做研究时才能保证策略里少一个暗坑结果可信度更高。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进