ARTICLE DETAIL

资讯详情

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

基于backtrader的ETH 15分钟短线策略回测:100次实验的流程与陷阱

基于backtrader的ETH 15分钟短线策略回测:100次实验的流程与陷阱 ETH 15分钟图表上的短线策略回测我最近完整跑了一轮前后共100次。这里说的100次不是100根K线而是把同一个回测框架放在不同参数、不同时间段上反复验证了100次。这篇文章把整个流程拆开写一遍适合刚接触backtrader、准备做加密货币K线回测或者已经能跑通demo但不知道下一步怎么验证策略的读者。最值得关注的不是某个策略收益率高不高而是回测流程怎么设计、参数怎么判断、100次实验里有哪些坑。先说明一点下面所有内容都是历史行情上的量化实验用来验证回测方法和代码逻辑。加密资产价格波动很大回测结果不能当作实盘收益预期更不能作为买卖依据。1. 先弄清15分钟短线回测到底在验证什么1.1 15分钟K线上短线策略的真正挑战15分钟周期处在“不算太快、也不算慢”的中间位置。相比1分钟15分钟单根K线包含的信息更完整噪音少一些相比日线15分钟又能容纳更多交易样本能在较短时间窗口内验证策略。但问题也在这里15分钟K线数量多交易信号也多策略一旦写得不严谨很容易在回测里频繁交易看起来热闹实际全是手续费。我这一轮实验用的是ETH的历史价格数据。ETH本身波动比较大15分钟级别经常会出现短时间内的连续上涨或下跌均线、动量这类趋势型指标会很敏感。短线策略要验证的并不是“能不能抓住每一段行情”而是在固定交易规则下历史数据里能不能形成稳定的盈亏结构。1.2 回测结论要能回答三个问题拿到一次回测结果后不要急着看收益率。先回答三个问题这个策略赚的钱到底来自趋势、均值回归还是单纯踩中了一段大行情如果改变参数收益是连续变化还是突然失效换到另一段时间的数据结果还成立吗这三个问题分别对应逻辑来源、参数敏感性、样本外稳定性。短线策略特别容易在这三关上翻车。很多回测报告看起来收益很高其实是把参数调到最优后选出来的展示结果换一段时间就不行了。所以我这一轮把100次实验分成了两类一类是参数网格测试另一类是不同时间窗口的滚动验证。两类实验共用同一套回测代码只是任务输入不同。1.3 100次实验的目标不是找“最好参数”很多人跑参数优化最后只取收益最高的那组参数这是最大的误区。100组参数里必然有一组在过去数据上表现最好但它不一定稳定。用少数几个参数组合跑到历史最高收益很容易过拟合。我这里的100次实验目标是把参数变化的结果记录下来观察“结果分布长什么样”。如果大部分组合都不赚钱只有个别组合特别好说明策略本身没有稳定优势如果大多数组合都能保持正收益并且参数小幅变化时结果变化不大才值得继续深挖。2. 回测环境准备数据、周期、行情口径2.1 准备一份干净的15分钟历史K线回测的第一步是数据这也是最容易出问题的一步。以ETH为例你需要一份包含时间、开盘价、最高价、最低价、收盘价、成交量的历史K线数据。我一般会把数据存成本地CSV文件字段类似下面这样datetime,open,high,low,close,volume 2024-01-01 00:00:00,2350.1,2358.5,2346.2,2355.3,1234.5拿到数据后先做三件事检查时间字段能不能解析成时间类型。检查是否存在重复时间戳。检查最高价是否小于最低价成交量是否为负数。这些看起来基础但在批量回测时影响很大。数据里有几根脏K线可能不直接影响单次回测但会在多次循环里反复污染结果。我建议第一次跑之前先打印数据的行数、起止时间和缺失值数量。如果你手里的原始数据不是15分钟而是1分钟或5分钟可以用pandas重采样到15分钟。这里只给一个通用的重采样思路import pandas as pd df pd.read_csv(eth_raw.csv, parse_dates[datetime]) df.set_index(datetime, inplaceTrue) df df.sort_index() agg_dict { open: first, high: max, low: min, close: last, volume: sum } df_15m df.resample(15min).agg(agg_dict).dropna()注意重采样不会自动修正原始数据里的错误。原始数据如果是坏的重采样后的结果仍然有隐患。我一般会在重采样前后都做一次最基础的校验。2.2 时间戳对齐和缺失值处理15分钟K线回测里时间戳的问题容易被忽略。常见情况有两种数据里混了UTC时间和本地时间。数据里缺少某些时间段。backtrader的PandasDataFeed本身不关心你用的是哪种时区只要整个数据内部一致就行。但如果你从不同来源拼接了CSV时间时区不统一K线的收盘、开盘顺序就会乱。我这100次实验里有几次结果突然全部变成0交易最后发现是某段数据出现了一个异常时间戳导致DataFeed没有正确加载。缺失值处理要看策略类型。如果策略用的是均线缺失的K线会打断指标计算如果只是按收盘价做状态判断可能影响相对小。稳妥做法是先看缺失时间段的占比占比很低的可以直接用前向填充占比高就考虑删掉这段区间或者换成更完整的数据。不要在数据有大量空洞的情况下批量跑100次那样只会得到一堆不稳定的结果。2.3 手续费、滑点和初始资金怎么配回测必须配置手续费和滑点否则15分钟级别的高频交易会在报表里显得“过于优秀”。手续费率不必写死但至少要有一个接近实际环境的估计值。我这里用万几级别的手续费作为示例真实交易成本还要结合你的实际费率、平台规则和流动性来判断。滑点方面短线策略可以使用“成交价比信号价差一点点”的方式模拟。我的经验是先在不需要精确模拟的情况下跑通再逐步加入滑点。不要一上来就设一个离谱的滑点这会掩盖策略本身的信号质量。初始资金也不宜设得太极端。设置得太少单笔交易比例稍微高一点就会被拒单设置得太多又会忽略仓位管理的影响。可以先设置一个中间值跑通后再调整。下面这张表是我在配置时常用的思路配置项示例值作用初始资金10000决定下单数量是否触发资金限制手续费0.001每笔交易扣除的成本滑点0.0005模拟价格偏离每笔仓位90%让单次订单尽量成交便于观察信号数据周期15分钟与策略信号周期一致这个配置只是示例具体数值要按自己的场景调整。重点是保持统一100次回测里使用同一套成本参数才能让不同参数组合之间的结果可以比较。3. 用backtrader把单次回测跑通3.1 策略代码框架我用的工具是backtrader它是一个开源的Python回测框架适合用来快速验证策略逻辑。第一次跑通不需要写很复杂的策略先上一版最简单的双均线策略目的是确认数据加载、订单生成、结果输出整条链路是通的。下面是一段最小策略示例import backtrader as bt class DualMaStrategy(bt.Strategy): params dict(fast10, slow30) def __init__(self): self.fast_ma bt.indicators.SMA(self.data.close, periodself.p.fast) self.slow_ma bt.indicators.SMA(self.data.close, periodself.p.slow) self.cross bt.indicators.CrossOver(self.fast_ma, self.slow_ma) self.order None def notify_order(self, order): if order.status in [order.Completed, order.Margin, order.Canceled, order.Rejected]: self.order None def next(self): if self.order: return if not self.position and self.cross 0: self.buy() elif self.position and self.cross 0: self.close()这个策略的逻辑很简单快线从下往上穿过慢线以后买入快线从上往下穿过慢线以后平仓。没有加止损也没有加仓位过滤。不要急着笑话它太简单。我的建议是第一版永远用最简单策略来验证流程。如果第一版就上复杂策略报错时你根本分不清是策略逻辑问题还是回测框架问题。3.2 回测引擎配置和运行策略写完后需要用backtrader组织数据、经纪商和交易费用。下面是单次回测的配置代码def run_single(df, fast, slow): cerebro bt.Cerebro() data bt.feeds.PandasData(datanamedf) cerebro.adddata(data) cerebro.addstrategy(DualMaStrategy, fastfast, slowslow) cerebro.broker.setcash(10000) cerebro.broker.setcommission(commission0.001) cerebro.addsizer(bt.sizers.PercentSizer, percents90) cerebro.addanalyzer(bt.analyzers.DrawDown, _namedd) cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _nametrades) cerebro.addanalyzer(bt.analyzers.SharpeRatio, _namesharpe, timeframebt.TimeFrame.Minutes, compression15, riskfreerate0) result cerebro.run() return cerebro, result[0]这里有几个点需要解释。PercentSizer表示按当前资金比例下单这里按90%仓位。设置90%是为了让每一笔订单都能尽量成交方便观察策略信号。如果你想更贴近实盘可以调低比例或者换成固定数量下单。setcommission设置的是默认真实交易成本不是象征性的0。很多新手忽略这一步导致回测结果看起来很漂亮但手续费一旦加上去正收益直接变成负收益。15分钟短线交易次数不少手续费对结果的影响非常明显。SharpeRatio分析器的时间和周期要跟数据周期匹配我这里写的是一份示例配置具体版本字段可能会有差异。如果你发现夏普比率算出来是空的先检查这里。3.3 从单次回测结果里先看什么跑完单次回测后先看三组数据最终资金和总收益率。交易笔数。最大回撤。如果交易笔数很少比如几百根K线只交易了两三次那这个参数组合的结果还不具备统计意义。如果交易笔数很多但最终资金低于初始资金先别急着否认策略看看是不是手续费过高、信号过于频繁。backtrader回测完成后可以用cerebro.broker.getvalue()拿到最终资金从分析器里取回撤和交易明细。这里我习惯打印一份简单结果final_value cerebro.broker.getvalue() print(f最终资金: {final_value:.2f}) print(f总收益率: {final_value / 10000 - 1:.2%}) dd strat.analyzers.dd.get_analysis() print(f最大回撤: {dd[max][drawdown]:.2f}%)第一次跑通时如果最终资金和初始资金完全一样说明根本没有订单生成。不用怀疑人生先检查策略信号是不是没触发再看DataFeed加载的行数是否正常。4. 100次回测的批量设计与过程管理4.1 为什么单次回测不够单次回测只能回答“这组参数在这段数据上表现如何”不能回答“这个策略是否稳定”。对于15分钟级别的短线策略市场状态变化很快一段时间的优秀结果很可能只是恰好贴合某段趋势行情。我这一轮跑了100次不是想用数量堆出一个“看起来靠谱”的结果而是要把策略放到不同条件和不同参数下观察。100次的数据量其实也不大关键是每一组结果都要有日志和原始记录方便后面复盘。4.2 批量任务两种设计参数网格与滚动窗口第一类任务是参数网格。假设快线周期从若干个值里选慢线周期从若干个值里选两个参数组合后就有大量组合。为了避免不合理参数我先过滤掉“快线周期大于等于慢线周期”的组合。剩下几十组再补一组基础参数形成第一批任务。第二类任务是滚动窗口。把完整的历史数据切成若干段比如每段4000根15分钟K线滑窗步进500根每段都用同一组参数回测。这样能看出策略在不同市场阶段的表现差异。我这里的100次并不全都是参数组合而是参数网格和滚动窗口的总和。这样做的好处是既能看到参数敏感性也能看到时间稳定性。如果你只想跑参数优化任务可以全部设为参数组合如果只想验证某套参数是否稳定任务就设为滚动窗口。具体用哪种设计取决于你想回答什么问题。不要因为别人说“参数优化要跑100次”就机械地跑100次。4.3 批量执行的日志、输出和失败恢复批量回测最怕的不是慢而是跑到一半挂了然后你不知道前面跑了哪些、哪些失败、失败原因是什么。我给每个任务加了一个唯一的任务ID同时把结果写进CSV或JSON文件。任务ID可以用参数组合和窗口索引来生成例如fast10_slow30_window_001每次任务开始前先打印任务ID和开始时间结束后打印任务ID、最终资金、交易笔数和最大回撤。如果异常捕获Exception并写入错误日志然后继续跑下一个任务而不是中断整个批处理。这里给一个批量回测的伪代码示例import itertools import json task_id 0 records [] for fast, slow in param_combinations: for window_index in [0, 1, 2, 3]: task_id 1 task_name ftask_{task_id:03d}_fast{fast}_slow{slow}_win{window_index} try: cerebro, strat run_single(window_df, fast, slow) records.append({...}) except Exception as e: print(f{task_name} 失败: {e}) # 把记录保存到文件为什么这里要单独捕获异常因为回测里经常出现某一组参数导致除零、空数据或者订单处理异常。如果不捕获整个批量任务会在第17次或第38次突然中断前面的结果也可能因为没及时保存而丢失。我一般每跑完一个任务就把结果追加写入CSV保证即使中断损失也只限于当前任务。有一个细节值得注意backtrader的DataFeed不建议反复复用。如果你的批量循环里把同一个DataFrame传给多个Cerebro可能会遇到第二次运行后不再触发任何信号问题。稳妥做法是每个任务都重新构造DataFeed或者保证DataFeed的数据是刚加载好的副本。这在批量回测里是我踩过最多的坑之一。注意批量回测最容易出的问题不是策略错了而是任务之间共享了同一个DataFeed第二次开始就再也不产生信号。每个任务都新建DataFeed是最省事的做法。5. 结果拆解哪些指标值得信哪些容易骗人5.1 核心指标判断标准100次回测跑完后把所有结果汇总成一张表。至少包含任务名、参数、总收益率、年化收益率、最大回撤、交易笔数、胜率、平均盈亏比、夏普比率。光看总收益率没有意义必须结合交易笔数和最大回撤。我一般会用几个简单规则做初筛交易笔数少于30笔的组合结果不参与后续判断。最大回撤超过20%的组合即使收益高也要额外标注。总收益率为正但平均盈亏比小于1需要警惕是否靠极少数大行情拉平。夏普比率在15分钟短线里不必强求很高但它能帮你比较不同参数组合的相对稳定性。指标之间也会冲突。比如某个参数组合收益很高但交易笔数只有12笔另一个组合收益一般但交易笔数80笔且回撤更小。这种情况下我一般会优先看重交易笔数和回撤因为样本量不足的收益数据可信度太低。5.2 最容易误读的回测数据胜率是最容易误读的指标。一个策略如果只有40%胜率但只要盈亏比能达到2以上长期也能赚反过来70%胜率但每笔亏损远大于每笔盈利一样可能把账户磨穿。15分钟短线经常出现连亏回测里胜率看着不低但遇到连续亏损时回撤会明显放大。年化收益率也需要小心。100次回测里如果用不同的数据长度和滚动窗口年化口径可能完全不同。你不能拿一段只有3个月的数据去推一年的收益也不能拿一段持续上涨的行情当基线。我的做法是除了总收益率固定用统一的数据长度来计算年化比如统一按“每4000根15分钟K线”计算这样不同任务之间才可比。还有一个容易骗人的数据是回撤恢复时间。只看“最大回撤是8%”不够还要看这个回撤发生在哪个阶段、花多少根K线恢复。有些策略在前期大幅回撤后期靠一段单边行情回到新高这种策略的实盘体验其实很差因为你可能根本拿不到后期。5.3 防止过拟合参数敏感性、样本外、交易次数下限100组参数里挑出收益最好的一组这种做法在学术上叫“选择偏差”在日常语言里叫过拟合。我用来减少过拟合的方法有三个。第一看参数邻域是否平滑。把fast和slow同时小幅变化如果收益连续变化、梯度平缓说明策略对参数不敏感如果只有某个点特别高周围稍微一点变化收益就变成负那这个高点大概率是过拟合出来的。100次回测结果的价值就在这里它能帮你画出参数敏感性的大致轮廓。第二做样本外验证。用前70%的数据跑参数网格选出表现相对稳定的区域再用后30%的数据验证。如果样本外表现明显变差说明参数并没有真正捕捉到市场规律只是拟合了历史噪声。样本外验证的方式不一定要很复杂关键是不要用全部数据去选参数最后又用同一批数据报告结果。第三设定交易次数下限。短线策略尤其要注意这一点。交易次数越少单次盈亏的偶然性越大。一组参数哪怕过去收益很高但只要只交易了10笔就不应该被当成有效结果。我在初筛时一般把30笔作为最低门槛低于这个数值的任务直接降权。6. 常见报错与排查顺序6.1 数据类问题100次回测里我遇到最多的问题还是数据。症状是某个任务突然没有任何交易或者结果跟其他任务差异巨大。排查顺序如下先看DataFeed加载了多少行。检查时间索引是否连续是否有重复时间戳。检查DataFrame是否有NaN。检查CSV里的时间列是否按升序排列。如果加载出来的数据行数跟预期差很多问题多半出在CSV解析或索引对齐上。backtrader的PandasData默认按DataFrame索引读取如果索引不是DatetimeIndex或者不是时间升序会出现加载成功但信号完全不触发的情况。重采样时也容易出问题。如果你从1分钟数据重采样到15分钟要确认15分钟K线的标签是这一段周期的起点还是终点。不同预期会导致最后一根K线标错进而影响指标计算。6.2 策略和引擎类问题策略类问题通常表现为订单一直处于提交状态、订单被拒绝、或者成交在奇怪的价格。backtrader的notify_order里如果订单状态一直不更新很可能是策略里没有正确释放self.order。我上面的示例里在订单完成、拒绝、取消等状态后会把self.order置为None这是为了不让后续信号被卡住。订单被拒绝时先看资金和仓位设置。初始资金太少、单笔仓位比例太高、手续费吃掉可用余额都会导致订单被拒。这个问题的排查价值在于它会让后续所有交易都失效你却只看到“没有交易”或者“收益率异常低”。均线类指标还需要关注最小周期。SMA(50)意味着前49根K线没有有效值策略的next可能在前面没有信号。这不是bug但如果你数据很短比如只有100根K线那策略实际有效区间只有后半段。批量比较时如果所有任务都使用相同数据长度这个影响是可控的。6.3 批量回测特有的问题批量回测会放大单次回测里不太明显的问题。第一个问题是内存增长。每个Cerebro都会持有策略、指标和数据如果循环里把上一轮的cerebro引用留着不释放跑几十次后内存就会明显上涨。我一般会在一次任务结束后把cerebro、strat置为None必要时调用垃圾回收。第二个问题是结果文件写入太频繁或太晚。太频繁会在异常中断时留下大量中间文件太晚则可能丢掉刚跑完的几十次结果。我建议每个任务结束就追加写入一行结果。记录里必须包含任务ID、参数、最终资金、交易笔数、最大回撤、异常信息等。这样即使中断你也能知道跑到哪个任务了。第三个问题是同一个DataFrame被多个任务共用。如果每个任务都传同一个DataFramebacktrader在第一次使用后可能不会报错但后续任务就是没有任何信号。这个问题我在前面已经提过处理方式就是每个任务重新构造DataFeed。6.4 通用排查顺序如果批量回测结果异常不要一头扎进策略代码里改参数。我一般的排查顺序是先确认数据加载的行数、时间范围、字段值是否正常。再确认单次回测随便取一组参数单独执行看是否有订单生成、成交价格是否合理。然后确认成本参数手续费和滑点是否导致订单被拒。接着检查批量逻辑任务之间是否共享了不该共享的数据对象。最后才是策略逻辑和参数设计。这个顺序的关键在于先排除环境和框架问题再怀疑策略本身。很多看起来像“策略失效”的现象根源都在数据和批量执行上。7. 回测结果和实盘之间的差距7.1 15分钟短线最容易被忽略的差距回测结果再好看也要清楚它和实盘之间的差距。15分钟短线最明显的差距有三个。第一是成交价。很多回测默认信号出现在当前K线收盘后并且按收盘价成交。但实际下单时你看到收盘价、触发信号、发送订单、订单成交这个过程中价格可能已经移动了。短线交易对执行延迟非常敏感15分钟虽然不像1分钟那么极端但也不能忽略。backtrader可以通过设置cheat_on_open来模拟部分更接近开盘价的成交方式但这不是万能方案还是要根据策略逻辑选择。第二是手续费、滑点在极端行情下的变化。回测里设置的是一个固定成本但遇到大波动时点差会扩大、成交可能更不利。如果策略的盈利本来就薄这些差异会直接改变结论。第三是流动性和最大持仓。高频短线回测里如果每次都是按90%仓位下单实盘中可能遇到流动性不够或者订单无法按预期数量成交。分批调整仓位后策略结果会跟回测有出入。注意回测里赚钱的参数组合不等于实盘也能赚钱。要先模拟跟踪再考虑是否投入真实资金。7.2 回测结论的下一步模拟跟踪而不是直接上实盘如果你在100次回测里发现了一组相对稳定的参数下一步不是急着投入真金白银而是先做一段模拟跟踪。模拟跟踪时记录信号出现时间、信号价格、实际成交价格、滑点差异、连续亏损情况观察回测里没有体现出来的问题。模拟跟踪的周期不能太短至少要覆盖一段反向行情。如果一个策略只在单边上涨的数据里表现好遇到横盘或下跌就会连续亏损。15分钟级别的横盘很常见所以模拟跟踪的重点不是“赚了多少”而是“遇到不利行情时最大回撤是否还在可接受范围”。这一步听起来不够刺激但它是过滤回测幻觉最有效的手段。很多人跳过模拟跟踪直接按回测参数上实盘之后才发现订单执行、滑点、心理压力每一项都在消耗收益。7.3 要不要往多品种、多周期扩展如果你后续想验证策略在其他品种上是否有效backtrader也支持多品种回测可以同时加载多个DataFeed。做多品种回测时要注意每一路的K线时间对齐、各品种之间的资金分配、信号合并和订单管理复杂度会比单品种高不少。这次只讨论ETH单品种但思路是通用的。扩展多品种前建议先确认当前单品种回测的流程足够稳定。连一个品种的日志、参数、结果都管理不好加再多品种只会让问题翻倍。7.4 最后几条经验回到100次回测本身我最后留几条经验回测首要目标是验证流程能跑通其次才是策略是否赚钱。流程不稳定所有结果都不可信。参数网格跑完后先看结果分布再选具体参数。不要只报告最优一组。手续费、滑点和数据质量决定了回测结果的可信度。这三项里只要有一项乱来后面全部白跑。15分钟短线交易次数多单次成本占比高。如果最终收益和总手续费接近这个策略基本没有实盘价值。批量回测一定要有日志、任务ID和失败恢复。跑了100次但丢了中间结果等于白跑。这些经验不是一次就能完整踩完的我也是跑了好几轮才慢慢把流程固定下来。如果你正准备做ETH或者其他币种的15分钟短线回测建议先按单次回测、参数网格、滚动窗口、模拟跟踪这个顺序来每一步都保留完整结果。这样最后得到的不是一张好看的成绩单而是一套经得起追问的实验记录。
返回列表