ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

量化策略回测源码解析:用Python+pandas打造可复现的交易流水线

量化策略回测源码解析:用Python+pandas打造可复现的交易流水线

简介:一套完整的量化策略源码包,面向量化交易初学者与策略研究者,覆盖个股日线行情从数据采集到模型评估的完整流程。压缩包内共18个文件,包括10个Python脚本、7个SQL数据库脚本和1个说明文档,整体仅56KB,体积精炼但模块划分清晰。源码按数据采集、数据预处理、SVM建模、模型评估、仓位管理五个环节组织,其中:Init_StockALL_Sp.py通过tushare接口拉取日线行情并存入本地数据库;DC.py将存储的日线行情整合为训练集;SVM.py对个股进行建模、训练与预测;Model_Evaluate.py结合回测与推进式建模计算精确率、召回率与F1分值;Portfolio.py基于马科维茨理论实现组合仓位管理。SQL脚本与README可辅助理解数据库结构与运行流程。该资源已有3568人学习,适合希望以最小成本快速搭建个人量化研究框架,并对照标准目录结构梳理实战思路的Python使用者。

1. 量化策略源码:不是“稳赚工具”,而是让你第一次把交易想法跑起来

你花了一个月写好的交易策略,在过去的K线上跑出翻倍的资金曲线,可一上实盘就连续打脸。问题不一定在策略本身,而在于缺了一套能严格复现回测过程的工程化框架。这份量化策略源码包做的就是这件事:把行情接入、信号计算、回测撮合、绩效分析串成一条可重复执行的流水线,避免你在手工复盘时漏掉手续费、未来函数这些致命细节。适合刚接触量化、想跑通第一个回测的新手,也适合手里有交易想法但总在回测中翻车、想找到原因的开发者。读完不仅能把它跑起来,还能照着模板把脑子里的策略落地成可回测的代码。

2. 整体架构与技术选型:三层分离,让策略与回测互不绑架

2.1 技术栈:Python + pandas + yaml 的轻量组合

选择Python做量化回测,绝大多数开发者的第一反应是pandas。原因很直接:K线数据本质是时间序列,按时间排序后做滚动计算、重采样、对齐,pandas都有现成方法,代码比for循环短一个量级。这份源码包沿用这个思路,没有引入复杂的分布式计算或事件驱动引擎,因为普通策略在单标的上做回测,用向量化计算就能快速跑完,裸用pandas的滚动窗口,效率完全够用。

配置没有写死在代码里,而是用yaml统一管理。yaml对嵌套结构友好,缩进式的写法比json少一堆括号,改参数时不容易改错。常见做法是把行情范围、策略名、策略参数、手续费率全部放到配置文件里,这样换品种、换时间窗口就不用改代码,只动配置。主流程读config,再把各个模块串起来。

为什么不直接用一个成熟的第三方回测框架?因为很多框架封装得太深,策略出问题时要查到底层逻辑很难。这份源码包刻意把回测引擎压缩到几百行,让你能在十分钟内读完撮合和绩效计算的全部逻辑。自己维护一个轻量回测虽然要处理更多边界情况,但对学习者和二次开发者来说,可控性远比功能全更重要。

2.2 目录结构:配置、数据、策略、回测、输出各管一摊

拿到源码包后,先看目录分布,心里有数再动手。整体是标准的按层分包:

quant/ ├── config/config.yaml # 行情范围、策略参数、回测成本 ├── data/ │ ├── fetcher.py # 从行情源拉取K线数据 │ └── cleaner.py # 数据清洗:去重、过滤、重采样 ├── strategies/ │ ├── base.py # 策略基类,定义统一接口 │ ├── ma_cross.py # 双均线策略 │ └── momentum.py # 动量突破策略 ├── backtest/ │ ├── engine.py # 向量化回测引擎 │ └── metrics.py # 夏普、最大回撤等指标计算 ├── analysis/ │ └── report.py # 生成资金曲线和绩效汇总 ├── main.py # 入口,按配置串起流水线 └── requirements.txt # 第三方依赖列表

data放数据层,strategies放策略层,backtest放回测层,analysis放输出层。每个目录只干一件事,避免逻辑混在一起。数据流是单向的:main先读config,然后fetcher拉数据,cleaner清洗,交给strategy生成position信号,engine按信号计算出equity资金曲线,metrics计算指标,report保存结果。每一层只依赖底层接口,比如strategy不关心数据是从哪里来的,只要给它一个含close列的DataFrame,它就算信号。

这样分层的好处是调试方便。回测结果不对,可以分段检查:数据是否干净、信号是否符合预期、撮合成本有无遗漏。而不用在几千行的单文件里找问题。我一般会把每个模块的输入输出打印出来,对比原始数据和中间结果,很快就能定位到是数据问题还是策略问题。

2.3 配置驱动:参数可调,避免为每组参数改一次代码

配置文件里最关键的是strategy和backtest两节。strategy节允许你切换不同类型策略,params传给策略类构造函数。backtest节控制初始资金、手续费、滑点。具体来说,data.symbol控制交易对或股票代码,start和end控制回测区间,timeframe控制K线级别;strategy.name控制使用哪个策略,params控制该策略的参数;backtest.initial_capital是初始资金,commission是双边手续费,slippage是滑点比例。

当你改了配置中的任一个参数,整个回测链路重新跑一遍,不需要再改代码。这个设计在做参数扫描时尤其省力,后面第6章会提到怎么用循环批量改参数。还有一点,配置里的成本参数最好不要设成0,不然你会在回测里看到很多在实盘中根本不存在的利润。

2.4 入口main.py:一个脚本把整条流水线串起来

有了模块划分,入口代码就可以写得很薄。它的作用只是按顺序调用各层函数:

# main.py import yaml from data.cleaner import clean_klines from strategies.ma_cross import generate_signal from backtest.engine import run from backtest.metrics import max_drawdown, sharpe_ratio def main(config_path: str): with open(config_path) as f: config = yaml.safe_load(f) # 拉取行情并清洗,这里只示意数据加载流程 df = load_kline_data(config["data"]) df = clean_klines(df) # 根据配置选择策略,生成 position 信号 if config["strategy"]["name"] == "ma_cross": df = generate_signal(df, **config["strategy"]["params"]) # 回测撮合,得到资金曲线 df = run(df, **config["backtest"]) # 计算绩效指标 dd = max_drawdown(df["equity"].values) sharpe = sharpe_ratio(df["strategy_returns"].values) print(f"最大回撤: {dd:.2%}, 夏普: {sharpe:.2f}") if __name__ == "__main__": main("config/config.yaml")

逻辑说明:main读入配置后,先加载数据并清洗,再按strategy.name动态选择策略,接着把data和backtest参数传给回测引擎,最后用metrics算指标并打印。整个流程没有任何业务逻辑混在入口里,所以想加日志、换数据源、扩展新策略都很方便。

参数说明:config_path是配置文件路径,默认相对路径,实际使用中我建议用绝对路径或命令行参数传入,避免因为工作目录不同而找不到配置文件。load_kline_data是一个示意函数,真实源码包里对应data/fetcher.py,这里为清晰简写了。

3. 核心模块拆解:行情数据如何变成买卖信号并生成资金曲线

3.1 数据清洗:把脏数据变成可交易的K线序列

行情源给的数据并不总是可信的:可能出现重复时间戳、平盘价格为0、短暂停牌导致的空值。直接拿来做回测,轻则计算出错,重则产生虚假信号。所以第一步是清洗。

# data/cleaner.py import pandas as pd def clean_klines(df: pd.DataFrame) -> pd.DataFrame: df = df.drop_duplicates(subset="timestamp").sort_values("timestamp") df = df[df["close"] > 0] df = df.fillna(method="ffill").dropna() return df.set_index("timestamp")

逻辑说明:先按timestamp去重,因为同一分钟重复的K线会让回测多出一根无效bar;然后过滤close为0或负数的异常行情,这类数据可能是行情源断线时的占位符;接着用ffill向前填充缺失的开高低收,再丢弃fill后仍为空的尾部。最后把timestamp设为索引,后面所有滚动计算都依赖这个时间索引,DataFrame的切片、resample就都方便了。

参数说明:df是原始K线DataFrame,至少需要timestamp、open、high、low、close、volume这些列。如果你接入的是不同格式的数据源,需要先把列名统一成这套约定,否则cleaner里引用df["close"]会直接报KeyError。这是最常见的接入新数据源时的翻车点。

3.2 信号生成:双均线策略在pandas里只需要四行核心逻辑

双均线是最经典的策略:快线上穿慢线做多,下穿做空。它的实现也很适合用来演示信号与持仓分离的思路。

# strategies/ma_cross.py import pandas as pd def generate_signal(df: pd.DataFrame, fast: int = 5, slow: int = 20) -> pd.DataFrame: df = df.copy() # 不要修改外部传入的数据 df["ma_fast"] = df["close"].rolling(fast).mean() df["ma_slow"] = df["close"].rolling(slow).mean() df["signal"] = 0 df.loc[df["ma_fast"] > df["ma_slow"], "signal"] = 1 df.loc[df["ma_fast"] < df["ma_slow"], "signal"] = -1 df["position"] = df["signal"].shift(1).fillna(0) # 关键:推迟一根K线,避免未来函数 return df

逻辑说明:rolling窗口计算均线,比较大小得到signal。signal为1表示持仓做多,-1表示空仓或做空,0表示无信号。这里最关键的代码是df["signal"].shift(1):它把信号推迟一根K线执行,模拟“收盘后计算信号,下一根开始交易”的真实场景。如果不shift,相当于你看到了当根K线收盘结果立刻成交,这就是未来函数,回测曲线会虚高。

参数说明:fast和slow是两条均线的窗口长度。窗口取太短,信号切换频繁,手续费消耗大;取太长,趋势反转时反应迟缓。常见做法是fast取5到10,slow取20到60,再按周期换算。比如在4小时线上,fast=5约等于20小时均线,适用波段级别行情。

3.3 回测引擎:手续费、滑点与持仓收益的对账

有了position序列,就能模拟每天的盈亏。回测引擎的核心是计算每日收益率,再扣掉交易成本。

# backtest/engine.py import pandas as pd def run(df: pd.DataFrame, initial_capital: float = 100000.0, commission: float = 0.0003, slippage: float = 0.0001) -> pd.DataFrame: df = df.copy() df["returns"] = df["close"].pct_change().fillna(0) df["strategy_returns"] = df["position"] * df["returns"] # 持仓状态乘以行情收益 position_change = df["position"].diff().abs().fillna(df["position"].abs()) df["cost"] = position_change * (commission + slippage) # 只在换仓时扣成本 df["strategy_returns"] -= df["cost"] df["equity"] = initial_capital * (1 + df["strategy_returns"]).cumprod() return df

逻辑说明:close的pct_change是收益率,position乘上收益率得到策略收益。position_change跟踪持仓变化,只有在换仓那一刻才收取手续费和滑点,避免持有期间每天重复扣费。cost是换仓成本,扣掉后累计收益率,乘初始资金就得到资金曲线equity。这段逻辑把撮合简化为:信号给出后下一根K线以开盘价成交,成本按换仓次数计。复杂撮合比如限价单未成交、涨跌停无法买入,这里都没有模拟,因为普通策略回测不需要那么细。

参数说明:commission是双边手续费率,slippage是单次滑点率。如果策略是T+0且换手频繁,这两项会极大影响结果。实盘费率通常不会像回测那样恒定,我一般会做一个成本敏感度测试,把cost乘以1.5和0.5分别跑一遍,看收益是否还有意义。

3.4 绩效指标:用夏普比率和最大回撤衡量策略质量

资金曲线出来以后,光看总收益不够,还要看承担了多少风险。一对最常用的指标是年化夏普比率和最大回撤。

# backtest/metrics.py import numpy as np def max_drawdown(equity: np.ndarray) -> float: running_max = np.maximum.accumulate(equity) return ((equity - running_max) / running_max).min() def sharpe_ratio(returns: np.ndarray, rf: float = 0.0, periods_per_year: int = 365) -> float: excess = returns - rf / periods_per_year return np.sqrt(periods_per_year) * excess.mean() / (excess.std() + 1e-12)

逻辑说明:最大回撤是从净值峰值到谷值的最大亏损比例,running_max计算截至每个时刻的历史最高净值,随后算回撤并取最小值。夏普比率是超额收益的标准差调整后的均值,乘以根号一年期数做年化。加1e-12是防止收益方差为0时除零报错,这在策略长期空仓时会遇到。

参数说明:periods_per_year取决于数据频率,日线用365或252都可,小时线可以传365*24。注意夏普比率基于正态分布假设,只适合同一策略内部比较,跨策略比较时最好结合最大回撤和胜率一起看。

4. 完整运行步骤:从依赖安装到看懂第一份回测报告

4.1 环境准备:创建虚拟环境并安装依赖

源码包已经准备好requirements.txt,用于锁定第三方库版本。运行前建议先创建虚拟环境,避免污染系统Python。

cd quant python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate pip install -r requirements.txt

逻辑说明:python -m venv创建隔离环境,source激活。requirements.txt里主要是pandas、numpy、pyyaml、matplotlib这几项。安装完成后可以用pip list确认关键包版本。这里特别提醒:不要图省事直接用系统Python装包,不同项目依赖互相打架,升级一次系统包就够你折腾半小时。

参数说明:venv目录名可以自定义,但建议固定为venv并在.gitignore里忽略它。requirements.txt里的版本号是经过验证的组合,不建议一次性全部升级到最新版,因为pandas API有时会有破坏性变更。

4.2 配置说明:每个字段都决定回测结果

运行前先看config/config.yaml。下面的配置是这份源码包自带的示例,覆盖了数据、策略、成本三段。

data: symbol: "BTCUSDT" start: "2020-01-01" end: "2023-12-31" timeframe: "1h" strategy: name: "ma_cross" params: fast: 5 slow: 20 backtest: initial_capital: 100000 commission: 0.0003 slippage: 0.0001

逐项说明:data.symbol是交易对名称,fetcher会用它去行情源拉K线;start和end是回测起止时间,区间越长越能覆盖不同行情阶段;timeframe是K线周期,改成“15m”或“1d”会影响策略参数的有效范围。strategy.name指定使用strategies目录下哪个策略文件,params传给策略构造函数。backtest里的initial_capital是初始资金,commission和slippage是交易成本。

注意一点:如果把timeframe改小,比如从1h改成15m,原来5和20的均线参数相当于缩小了4倍,信号会变得异常频繁。我经常看到有人只改周期不改参数,然后拿着回测结果到处问为什么收益差,其实就是参数和周期不匹配。

4.3 运行回测与解读报告

配置无误后,执行:

python main.py --config config/config.yaml

main.py会依次执行数据拉取、清洗、信号生成、回测、指标计算,最后在analysis/output目录下写入资金曲线图和performance.csv。终端会打印一份绩效汇总,类似这样:

初始资金: 100000.00 期末资金: 123456.78 总收益率: 23.46% 年化收益率: 24.51% 最大回撤: -8.32% 夏普比率: 1.56 胜率: 54.21% 交易次数: 127

解读这些数字的关键在于组合看:总收益率高但最大回撤也高,说明策略波动大,拿在手里很难受;夏普比率大于1说明单位风险回报可以接受,低于1就要警惕收益是否来自少数几次极端行情。交易次数和胜率一起看,如果胜率很高但总收益一般,大概率是“高胜率低赔率”策略,盈利被大亏损一笔打回。报告里的资金曲线图不是让我们看着开心,而是检查曲线是否平滑上升、有没有单笔异常跳变。

4.4 运行失败时先看这五个位置

跑不起来的时候不要急着改代码,先按顺序排查:第一,依赖是否装全,重新执行pip install -r requirements.txt并观察报错;第二,配置文件路径对不对,工作目录是否在quant目录下;第三,数据源是否可用,fetcher有没有打印拉取失败或空数据提示;第四,策略名称是否和strategies目录下的文件名一致,不一致会走到None分支;第五,回测区间内数据量是否太少,比如只给了十几天日线,均线窗口还没填满就结束了。这几类问题占了运行问题的绝大多数。

5. 回测避坑指南:四个让我亏过钱的“隐形杀手”

5.1 未来函数:资金曲线为什么比想象中漂亮

现象:回测里年化收益50%,参数怎么改都不会大亏,但实盘连续打脸。原因:最常见的是用当根K线收盘价计算信号,又在同一根K线的第二秒成交。双均线策略尤其容易踩这个问题,因为ma_fast和ma_slow用到的close包含当根K线的收盘,而你的代码可能直接按这个信号当天买入。解决:所有信号必须shift(1),再用下一根K线的open作为成交价。在engine里,我还会加一个断言,强制执行了shift的信号才会进入回测,否则直接报错,从机制上杜绝未来函数。

5.2 幸存者偏差:选股池里为什么没有失败者

现象:选股策略回测胜率80%,但实盘选出来一股就跌。原因:代码里选股时用了“当前时点”已知的股票列表,把后来退市的股票全排除了。回测只看到活下来的上市公司,自然觉得策略很神。解决:做多标的回测时,要使用历史时点的成分股列表,并把已经退市的股票也包含进股票池,模拟真实情况下你面临的全集。如果手头没有历史成分数据,至少要做一次反向测试——在包含夕阳标的的池子里跑,看结果会不会崩。

5.3 手续费与滑点:高频策略最容易被成本抹平

现象:30分钟线的动量策略回测胜率超过60%,但实盘每月手续费比利润还高。原因:手续费设成万分之一甚至0,滑点完全忽略。回测时每次换仓只算一点毛利,但实盘有买卖价差、冲击成本和最低佣金。解决:先按双边万三加上每笔几元的最低佣金来测试,再把滑点设为盘口平均价差的50%。我一般会把成本参数放大两倍再跑一遍还能不能赚钱,以此评估策略的容错空间。

5.4 过拟合:参数换来换去,换来一场空

现象:在5到120的区间内逐个测试均线参数,终于找到一组收益最漂亮的,但换到另一个品种或时间段就失效。原因:反复使用同一段历史数据进行参数筛选,本质上是在拟合历史噪声。样本量越小、参数越多,过拟合越严重。解决:把历史数据切成训练集和测试集,只在训练集上调参,测试集最多跑一次。如果最优参数在测试集上依然表现不错,才说明策略逻辑本身有信号,而不是参数记住了K线。

6. 二次开发技巧:把你的交易想法变成可回测策略

6.1 策略模板:继承基类,实现generate_signal即可

源码包里strategies/base.py定义了统一接口:

# strategies/base.py import pandas as pd class BaseStrategy: def __init__(self, params: dict = None): self.params = params or {} def generate_signal(self, df: pd.DataFrame) -> pd.DataFrame: raise NotImplementedError

新增策略时只要继承这个基类,实现generate_signal,返回带position列的DataFrame。你可以参考ma_cross的写法,先复制一份代码,改均线参数为布林带或MACD逻辑。注意保持输入输出约定:入参必须含close列,出参必须含position列,position范围是-1/0/1,并且已经shift(1)。这样回测引擎和绩效模块不用动,就能跑新策略。

6.2 参数优化:先画参数平原,再谈最优参数

我不建议直接套网格搜索找最大值。常见做法是先在一个小范围内跑4到6组参数,画出表现随参数变化的曲线。如果收益和回撤在参数空间中像锯齿一样剧烈跳动,说明策略不稳定,继续优化只会过拟合。只有在参数平原上,相邻参数的结果变化平缓,你才可以说这个策略具备稳健性。参数最优化做完后,一定留出一段样本外数据做最终验证。

从那以后,我每次新增策略都强制走一遍样本内调参、样本外验证的流程,不再被最优参数和漂亮回测曲线欺骗。希望帮到你。

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

RELATED READING

延伸阅读

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