Blankly / 回测引擎审计
漂亮回测背后:三处默认设置会系统性抬高你的收益
Blankly 的仓库里有一份专门讲回测引擎设计取舍的工程文档,中文圈几乎没人引用。把这份文档和配置默认值放在一起看,会得到三个很具体的结论:配置里默认没有基准对照、默认无风险利率为零、离线模式默认手续费为零;而工程文档又明确写着当前没有滑点模型、假设无限挂单流动性。这四点叠在一起,意味着默认情况下跑出来的收益是偏乐观的。这一页把三层审计项摊开,并给出一份可复现记录清单。
BACKTESTING_ENGINEERING.md 与 examples/backtest.json 默认值整理,非官方评分标准)。四格分别对应本页的四张表。哪些默认值该最先检查?三处不需要写错代码就会发生的问题
这三条都在官方示例配置文件与构造函数签名里,属于「不配置就是这样」的默认行为。它们的影响方向一致:让表面收益更好看。
| 默认项 | 默认值 | 它意味着什么 | 你应该做什么 |
|---|---|---|---|
benchmark_symbol | null | 回测默认不与任何基准比较,你看到的只有绝对收益 | 显式设置一个基准标的,或在结论里同时给出买入持有对照 |
risk_free_return_rate | 0.0 | Sharpe / Sortino 等指标按「无风险利率为零」计算 | 按你的研究口径设定该值,并写进记录;跨策略比较时要统一 |
KeylessExchange(maker_fee, taker_fee) | 都为 0 | 用离线数据自测策略时,手续费是零,交易摩擦完全不计 | 显式传入与你实际费率档位相符的数值 |
use_price | close | 默认用收盘价作为价格序列 | 确认这与你的信号生成时点一致;用其它字段要显式改 |
GUI_output | true | 默认会生成图形输出(依赖 bokeh) | 无头环境或批处理时关掉,避免无谓开销与失败点 |
ignore_user_exceptions | true | 回测会忽略你在回调里抛出的异常 | 调试阶段建议关掉,否则策略静默失效而你收不到信号 |
continuous_caching / cache_location | true / ./price_caches | 价格数据会持续落盘缓存 | 注意缓存与数据源口径的一致性;换源时清理缓存目录 |
resample_account_value_for_metrics | 1d | 账户价值按日重采样后再算绩效指标 | 换成其它频率会直接改变所有年化类指标,务必记录该值 |
哪些模拟是精确的?值得肯定的几处机制
审计不等于只挑毛病。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 / cavr | 有 | VaR 与条件风险价值。键名是 cavr 而不是常见的 cvar | 按 cvar 取值会 KeyError;以源码键名为准 |
resampled_time / risk_free_rate | 有 | 把所用口径直接回显在结果里(如 86400.0 秒、0.0) | 保留这两个字段,它们是口径证据 |
| 交易次数 | 无 | 12 个汇总字段里没有交易笔数 | 自己从 trades 统计 |
| 胜率 | 无 | 框架不提供胜率字段 | 自己按逐笔盈亏计算,并列出口径(是否含手续费) |
get_quantstats_metrics() | 有,但需额外依赖 | 可产出更完整的绩效报告 | 确认该第三方库版本与兼容性 |
| 基准对照 | 需显式配置 | 默认 benchmark_symbol 为 null | 自己设基准,或另算买入持有 |
怎么让别人能重跑你的回测?一份记录清单
看到一个回测数字却不知道它怎么产生的,是最常见的问题。下表把该记录的东西列全,并对应到 Blankly 的具体位置。
| 要记录的字段 | 为什么重要 | 在 Blankly 里对应到哪里 |
|---|---|---|
| Blankly 版本与 Python 版本 | 依赖行为随版本变化,离线模式还要求钉 numpy | pip freeze 的 blankly== 与 numpy== 两行 |
| 数据来源与获取时间 | 同一标的在不同交易所价格不同;历史还会被修订 | 交易所接口名,或自备文件的生成脚本与生成时间 |
| 数据分辨率与区间 | 决定限价单能否成交、以及回测能推进多远 | 读取器推断出的分辨率;backtest() 的起止时间 |
| 价格字段 | 收盘价与其它字段会给出不同结果 | backtest.json 的 use_price |
| 费率假设 | 离线模式默认零费,这是最容易被忽略的一项 | KeylessExchange 的 maker_fee / taker_fee |
| 滑点假设 | 引擎当前没有滑点模型,你的假设必须自己声明 | 框架外实现,写进方法论说明 |
| 基准与无风险利率 | 没有对照就没有结论;Sharpe 口径取决于无风险利率 | benchmark_symbol、risk_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_rate 与 resampled_time 回显值 | 统一无风险利率与重采样频率后再横向比较 |
| 有信号但一笔都没成交 | 订单被交易所 order filter 拦下(数量增量、最小下单量、限价范围等) | 回看日志与 trades 是否为空 | 按实际交易规则调整下单数量与价格;确认标的代码正确 |
| 限价单在明显能成交的位置没成交 | 引擎不推断比数据更高分辨率的价格 | 提高数据分辨率重跑,比较成交差异 | 使用更高分辨率数据,或在结论中声明这一近似的存在 |
| 策略静默失效却看不出原因 | ignore_user_exceptions 默认为 true | 关掉该开关重跑,观察是否抛出异常 | 调试阶段关闭异常忽略,稳定后再按需开启 |
关于回测可信度的高频问题
本页的框架侧结论可在仓库文件与实测中复核;涉及投资决策的部分请自行判断。
没有滑点模型,是不是说 Blankly 的回测完全没用?
不是。滑点缺失对不同策略的影响差别很大:低频、低换手、大盘标的策略受影响有限;高频或高换手策略受影响可能很大。正确做法不是放弃回测,而是在结论里显式声明「本次回测未计入滑点」,然后补一次压力测试——比如按成交名义额扣减若干基点,看策略是否仍然成立。官方也把滑点模型列在「建议特性」里,说明作者本人认为这是应当补齐的能力。
官方文档说「一年回测 3 秒」,这个数字可靠吗?
本站没有复现这个基准,因此不做判断。可以确定的是:这句话出自仓库的工程文档,属于官方自述,比较对象是另一个开源引擎,没有公布测试硬件、数据规模和策略复杂度。引用它时应当标注来源,不要当成本站独立测量的数据。
为什么离线回测默认没有手续费?
因为在离线模式下,引擎无法从交易所读取你的费率档位(那需要密钥和账户权限),而构造函数给 maker_fee 与 taker_fee 的默认值就是 0。这个默认值本身是合理的,问题在于很多人不知道它是 0。用离线数据做策略自测时,这是最需要主动补上的一项。
cavr 和 cvar 是同一个东西吗?
从含义上,结果里的 cavr 字段显示名为 Conditional Value-at-Risk,就是条件风险价值。但键名是 cavr——多了一个字母 a。用 cvar 去取值会抛 KeyError。以源码的实际键名为准,先打印一次 metrics.keys() 确认。
我想看「回撤发生在什么时候」,怎么拿?
汇总指标只给最大回撤的数值,没有回撤序列。要看时间分布,需要走数据路径:用 get_returns() 拿账户价值序列(实测返回 pandas DataFrame,列为 time / value),自己算回撤序列,再对照时间轴看集中在哪个区间。这一步通常是判断「是结构性问题还是特定时代背景」的关键。
把基准、无风险利率、费率都配好,结论就能信了吗?
还不够。这三项只解决了「口径」问题,还有两件事必须做:一是样本外检验(参数在样本内定、结论只在样本外下),二是参数敏感性(看是否存在只有某个神奇参数值才能赚的尖峰)。前者框架不帮你做,后者框架也不帮你做——它们属于研究方法,而不是工具功能。