Blankly / 回测引擎审计

漂亮回测背后:三处默认设置会系统性抬高你的收益

Blankly 的仓库里有一份专门讲回测引擎设计取舍的工程文档,中文圈几乎没人引用。把这份文档和配置默认值放在一起看,会得到三个很具体的结论:配置里默认没有基准对照、默认无风险利率为零、离线模式默认手续费为零;而工程文档又明确写着当前没有滑点模型、假设无限挂单流动性。这四点叠在一起,意味着默认情况下跑出来的收益是偏乐观的。这一页把三层审计项摊开,并给出一份可复现记录清单。

依据:BACKTESTING_ENGINEERING.md依据:examples/backtest.json依据:实测 Keyless 签名本站未实机交易
模拟精度层订单过滤器 / 费率来源 / 限价单与数据分辨率
成本口径层无滑点模型 / 无限流动性假设
结果口径层12 个字段 / 无交易次数与胜率 / 重采样口径
默认缺口无基准 / 无风险利率 0 / 离线零手续费
回测可信度分层示意(依据仓库 BACKTESTING_ENGINEERING.mdexamples/backtest.json 默认值整理,非官方评分标准)。四格分别对应本页的四张表。
默认缺口

哪些默认值该最先检查?三处不需要写错代码就会发生的问题

这三条都在官方示例配置文件与构造函数签名里,属于「不配置就是这样」的默认行为。它们的影响方向一致:让表面收益更好看。

默认项默认值它意味着什么你应该做什么
benchmark_symbolnull回测默认不与任何基准比较,你看到的只有绝对收益显式设置一个基准标的,或在结论里同时给出买入持有对照
risk_free_return_rate0.0Sharpe / Sortino 等指标按「无风险利率为零」计算按你的研究口径设定该值,并写进记录;跨策略比较时要统一
KeylessExchange(maker_fee, taker_fee)都为 0用离线数据自测策略时,手续费是零,交易摩擦完全不计显式传入与你实际费率档位相符的数值
use_priceclose默认用收盘价作为价格序列确认这与你的信号生成时点一致;用其它字段要显式改
GUI_outputtrue默认会生成图形输出(依赖 bokeh)无头环境或批处理时关掉,避免无谓开销与失败点
ignore_user_exceptionstrue回测会忽略你在回调里抛出的异常调试阶段建议关掉,否则策略静默失效而你收不到信号
continuous_caching / cache_locationtrue / ./price_caches价格数据会持续落盘缓存注意缓存与数据源口径的一致性;换源时清理缓存目录
resample_account_value_for_metrics1d账户价值按日重采样后再算绩效指标换成其它频率会直接改变所有年化类指标,务必记录该值
把这三条串起来看:默认没有基准 + 无风险利率为零 + 零手续费,再叠加下面要讲的「没有滑点模型」。任何一份用 Blankly 默认配置产出的收益曲线,都需要先把这四项补齐,才有讨论价值。
模拟精度层

哪些模拟是精确的?值得肯定的几处机制

审计不等于只挑毛病。Blankly 的引擎在几件具体的事上做了比多数轻量回测框架更细的工作,这些是它真正的竞争力所在。

机制官方工程文档的表述它带来的好处仍需注意的边界
订单过滤器校验每笔交易都会按交易所的 order filter 校验,共评估 10 项限制:限价单最小/最大数量、基础数量增量、限价价格增量、限价最大最小价格、是否支持小数、买卖方名义额差异、限价百分比范围(如某交易所 ±25%)避免回测中出现实盘不可能成交的订单,减少「回测能赚、实盘下不出去」的落差限制参数以交易所实际返回为准,接口变动时会失配
数据来自用户交易所价格数据从用户所连的交易所拉取并缓存,理由是不同交易所同一资产的价格可能差异显著避免用第三方聚合价回测后在实盘对不上换交易所等于换数据源,历史结果不可直接横向比较
费率按档位下载费率从用户交易所按实际费率档位下载(文档举例:在 Coinbase Pro 上成交额落在 100k 到 1M 区间时,taker 0.20% / maker 0.10%)高成交量用户的低费率会被正确反映需要读取账户权限;离线模式拿不到,只能手填
限价单逐次评估回测中所有限价单在每次价格变动时都重新评估比只在固定时点检查限价单更接近真实引擎不会推断比已下载数据更高分辨率的价格——文档原话是「限价买 1000 美元的单,价格只跌到 999 不会成交」
市场开收盘会评估开盘与收盘;订单可以处于 pending 状态并在开盘价附近成交对股票类标的更贴近真实撮合官方文档说明该行为将随着接入更多股票交易所而扩展
卖空与保证金在 Alpaca 上精确评估卖空,持续按 Alpaca 的保证金规则校验购买力做空类策略不会得到虚高结论官方表述为「可扩展到其它交易所供实验」,即默认只在 Alpaca 上成立
成本口径层

哪些假设被简化了?这是结论失真的主要来源

这一层的每一条都会让回测结果偏离真实成交,而且偏离方向通常是「往好看的一侧偏」。

假设 / 缺口官方工程文档的表述对结论的影响方向你可以怎么缓解
滑点模型缺失「当前回测引擎假设无限的挂单流动性,意味着订单按当前价精确成交。对高成交量或高频交易者,这往往不准确。」官方把滑点模型列在 Suggested Features(建议特性)里,属于尚未实现高频、大额策略的收益被系统性抬高把滑点当作事后压力测试:按成交名义额加 N 个基点,看结论是否仍然成立
成交价等于当前价同上一行,是「无限流动性」假设的直接结果换手越高的策略被抬高越多对比不同换手水平下的敏感性
限价单不推断更高分辨率引擎不会考虑比已下载数据更高分辨率的价格变化可能漏掉真实成交,也可能错过实际上能成交的单提高数据分辨率重跑,比较成交笔数是否变化
期货资金费率处理未完成「期货引擎仍在 beta,目前能正确按价格评估买卖;资金费率的下载与缓存仍在开发中」长期持有的期货策略缺少一项持续成本自己估算资金费率并作为额外成本扣减
事件驱动而非向量化官方采用事件驱动方式,自述「比向量化慢得多但更真实」,并声称一年期回测可在 3 秒内完成(对比对象为另一个开源引擎的 5 分钟以上)方向上是好事(更接近真实顺序)注意:该性能对比是官方自述,本站未复现,不要当作独立测量结果引用
卖空能力的交易所范围精确做空目前主要在 Alpaca 上实现,其它交易所为实验性扩展在其它交易所上跑做空策略可能得到过于乐观的结果把「该交易所是否支持精确做空」写进你的记录
最容易被忽略的是第一条。滑点缺失对低频、低换手、大盘标的策略影响有限;但一旦策略年换手率很高,滑点往往就是策略是否还有超额收益的分水岭。如果你的回测里没有任何滑点假设,那么「策略有效」这个结论其实还没有被检验。
结果口径层

回测结果里有什么、缺什么?实测字段清单

本机在离线数据上跑通一次回测后,直接打印了结果对象的字段。下面这张表是实测结果,不是文档摘抄。

字段 / 能力状态说明你需要补什么
calmar / sortino / sharpe三个常见风险调整收益指标都在确认无风险利率与年化口径,默认 risk_free_return_rate=0.0
cagr / cum_returns年化增长率与累计收益,均为百分比确认重采样频率 resample_account_value_for_metrics
max_drawdown最大回撤(百分比)只有最大值,没有回撤时间序列;需要看「何时回撤」要自己取
volatility / variance波动率与方差注意方差字段名带百分号语义
value_at_risk / cavrVaR 与条件风险价值。键名是 cavr 而不是常见的 cvarcvar 取值会 KeyError;以源码键名为准
resampled_time / risk_free_rate把所用口径直接回显在结果里(如 86400.0 秒、0.0)保留这两个字段,它们是口径证据
交易次数12 个汇总字段里没有交易笔数自己从 trades 统计
胜率框架不提供胜率字段自己按逐笔盈亏计算,并列出口径(是否含手续费)
get_quantstats_metrics()有,但需额外依赖可产出更完整的绩效报告确认该第三方库版本与兼容性
基准对照需显式配置默认 benchmark_symbolnull自己设基准,或另算买入持有
为什么要强调「没有交易次数与胜率」:中文圈里常能看到「胜率 100%」这类标题,但 Blankly 的结果对象里根本没有胜率字段。如果一篇文章给出胜率,那个数字一定是作者自己算的——这时候就该追问:几笔交易?是否含手续费?是否只统计已平仓交易?
可复现记录

怎么让别人能重跑你的回测?一份记录清单

看到一个回测数字却不知道它怎么产生的,是最常见的问题。下表把该记录的东西列全,并对应到 Blankly 的具体位置。

要记录的字段为什么重要在 Blankly 里对应到哪里
Blankly 版本与 Python 版本依赖行为随版本变化,离线模式还要求钉 numpypip freezeblankly==numpy== 两行
数据来源与获取时间同一标的在不同交易所价格不同;历史还会被修订交易所接口名,或自备文件的生成脚本与生成时间
数据分辨率与区间决定限价单能否成交、以及回测能推进多远读取器推断出的分辨率;backtest() 的起止时间
价格字段收盘价与其它字段会给出不同结果backtest.jsonuse_price
费率假设离线模式默认零费,这是最容易被忽略的一项KeylessExchangemaker_fee / taker_fee
滑点假设引擎当前没有滑点模型,你的假设必须自己声明框架外实现,写进方法论说明
基准与无风险利率没有对照就没有结论;Sharpe 口径取决于无风险利率benchmark_symbolrisk_free_return_rate
账户价值重采样频率直接改变年化类指标resample_account_value_for_metrics
初始资金与计价资产决定仓位与收益率的基数backtest(initial_values={'USD': ...}) 的键与值
异常处理开关影响你是否能发现策略静默失效ignore_user_exceptions
样本外区间与参数选择依据没有样本外,参数很容易是拟合出来的框架外的方法论,需自己切分区间
排查表

回测中最容易「看起来对但其实错」的六种场景是什么?

下面每一行都对应 Blankly 的一个具体机制或默认值。它们的共同点是:代码没有报错,结果看起来很漂亮,但结论站不住。

症状背后的机制怎么发现怎么修
收益曲线平滑得不像真实交易离线默认零手续费 + 无滑点模型,摩擦完全没有计入把费率从 0 改成一档真实值重跑,看收益衰减幅度显式传费率;高换手策略额外做滑点压力测试
策略「跑赢大盘」但没有基准benchmark_symbol 默认为 null检查结果里是否存在任何相对收益字段显式设置基准,或另算买入持有曲线并列展示
Sharpe 比其它工具算出来的高一截默认 risk_free_return_rate=0.0,且年化取决于重采样频率对比 metrics 里的 risk_free_rateresampled_time 回显值统一无风险利率与重采样频率后再横向比较
有信号但一笔都没成交订单被交易所 order filter 拦下(数量增量、最小下单量、限价范围等)回看日志与 trades 是否为空按实际交易规则调整下单数量与价格;确认标的代码正确
限价单在明显能成交的位置没成交引擎不推断比数据更高分辨率的价格提高数据分辨率重跑,比较成交差异使用更高分辨率数据,或在结论中声明这一近似的存在
策略静默失效却看不出原因ignore_user_exceptions 默认为 true关掉该开关重跑,观察是否抛出异常调试阶段关闭异常忽略,稳定后再按需开启
把这张表当成上线前的检查单:六条里只要有一条没有确认,回测结论就还不能作为决策依据。本站不提供「达到某分数才算通过」的标准,因为标准取决于你的策略类型、资金规模和风险容忍度。
FAQ

关于回测可信度的高频问题

本页的框架侧结论可在仓库文件与实测中复核;涉及投资决策的部分请自行判断。

没有滑点模型,是不是说 Blankly 的回测完全没用?

不是。滑点缺失对不同策略的影响差别很大:低频、低换手、大盘标的策略受影响有限;高频或高换手策略受影响可能很大。正确做法不是放弃回测,而是在结论里显式声明「本次回测未计入滑点」,然后补一次压力测试——比如按成交名义额扣减若干基点,看策略是否仍然成立。官方也把滑点模型列在「建议特性」里,说明作者本人认为这是应当补齐的能力。

官方文档说「一年回测 3 秒」,这个数字可靠吗?

本站没有复现这个基准,因此不做判断。可以确定的是:这句话出自仓库的工程文档,属于官方自述,比较对象是另一个开源引擎,没有公布测试硬件、数据规模和策略复杂度。引用它时应当标注来源,不要当成本站独立测量的数据。

为什么离线回测默认没有手续费?

因为在离线模式下,引擎无法从交易所读取你的费率档位(那需要密钥和账户权限),而构造函数给 maker_feetaker_fee 的默认值就是 0。这个默认值本身是合理的,问题在于很多人不知道它是 0。用离线数据做策略自测时,这是最需要主动补上的一项。

cavrcvar 是同一个东西吗?

从含义上,结果里的 cavr 字段显示名为 Conditional Value-at-Risk,就是条件风险价值。但键名是 cavr——多了一个字母 a。用 cvar 去取值会抛 KeyError。以源码的实际键名为准,先打印一次 metrics.keys() 确认。

我想看「回撤发生在什么时候」,怎么拿?

汇总指标只给最大回撤的数值,没有回撤序列。要看时间分布,需要走数据路径:用 get_returns() 拿账户价值序列(实测返回 pandas DataFrame,列为 time / value),自己算回撤序列,再对照时间轴看集中在哪个区间。这一步通常是判断「是结构性问题还是特定时代背景」的关键。

把基准、无风险利率、费率都配好,结论就能信了吗?

还不够。这三项只解决了「口径」问题,还有两件事必须做:一是样本外检验(参数在样本内定、结论只在样本外下),二是参数敏感性(看是否存在只有某个神奇参数值才能赚的尖峰)。前者框架不帮你做,后者框架也不帮你做——它们属于研究方法,而不是工具功能。

下一步:从回测走到实盘要重验什么

回测口径补齐之后,最后一道坎是实盘。Blankly 的官方模板用「同一份代码切换环境」表达这一能力,但真实世界里需要重验的东西远多于一行代码。