关于 backtesting-py-oracle
为使用 backtesting.py 的量化研究者提供验证 ClickHouse SQL 扫描结果的配置与错误规避指导。重点处理重叠交易、数据对齐和图表缩放等问题,并以五项指标核对 Python 与 SQL 结果。
主要功能
- 01配置多持仓回测以匹配 SQL
- 02识别交易排序与时间戳差异
- 03处理滚动分位数中的 NaN
- 04按五项指标验证结果
- 05修正缩放时的图表纵轴
适用场景
- 01核对 SQL 与 Python 的交易数量
- 02诊断入场价格或时间不一致
- 03检查跨资产信号过滤偏差
- 04缩放查看权益曲线局部数据
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ terrylica/ cc-skills --skill backtesting-py-oracle安装后在 Claude Code 中输入/backtesting-py-oracle确认已加载。
附带文件references/evolution-log.md
详细介绍
backtesting.py 区间柱模式的 Oracle 验证
用于使用 backtesting.py 验证 ClickHouse SQL 扫描结果的配置和反模式。确保 SQL 和 Python 交易评估之间实现位级原子可复现性。
配套技能:clickhouse-antipatterns(SQL 正确性,AP-16)| sweep-methodology(扫描设计)| rangebar-eval-metrics(评估指标)
已验证:Gen600 Oracle 验证(2026-02-12)——3 个资产、5 个关卡,全部通过。
自我演进技能:此技能会在使用过程中持续改进。如果说明有误、参数发生漂移或需要变通方案,请立即修复此文件,不要拖延。仅针对真实且可复现的问题进行更新。
关键配置(绝不可省略)
from backtesting import Backtest
bt = Backtest(
df,
Strategy,
cash=100_000,
commission=0,
hedging=True, # REQUIRED: Multiple concurrent positions
exclusive_orders=False, # REQUIRED: Don't auto-close on new signal
)
原因:SQL 会独立评估每个信号(允许交易重叠)。如果没有 hedging=True,当已有持仓时,backtesting.py 会跳过信号,导致交易数少于 SQL。此问题是在发现 SOLUSDT 的 Python 交易数为 105,而 SQL 交易数为 121 时确认的——16 个信号被悄然跳过。
反模式(按严重程度排序)
BP-01:缺少多持仓模式(严重)
症状:Python 产生的交易数少于 SQL。关卡 1(信号数量)未通过。
根本原因:默认的 exclusive_orders=True 会阻止在已有持仓时开立新仓位。
修复:始终使用 hedging=True, exclusive_orders=False。
BP-02:ExitTime 排序顺序(严重)
症状:尽管 SQL 和 Python 使用相同的价格来源,入场价格仍然不匹配(关卡 3 未通过)。
根本原因:stats._trades 按 ExitTime 排序,而非 EntryTime 排序。当重叠交易的退出顺序与入场顺序不同时,trade[i] 不再对应 signal[i]。
修复:
trades = stats._trades.sort_values("EntryTime").reset_index(drop=True)
BP-03:滚动分位数中的 NaN 污染(严重)
症状:跨资产测试失败,Python 交易数远少于预期。特征分位数变为 NaN,并无限向后传播。
根本原因:输入包含 NaN 时,np.percentile 会返回 NaN。只要有一个 NaN 特征值进入滚动窗口,之后的所有分位数都会变为 NaN,导致之后所有筛选条件比较均失败。
修复:构建信号窗口时跳过 NaN 值:
def _rolling_quantile_on_signals(feature_arr, is_signal_arr, quantile_pct, window=1000):
result = np.full(len(feature_arr), np.nan)
signal_values = []
for i in range(len(feature_arr)):
if is_signal_arr[i]:
if len(signal_values) > 0:
window_data = signal_values[-window:]
result[i] = np.percentile(window_data, quantile_pct * 100)
# Only append non-NaN values (matches SQL quantileExactExclusive NULL handling)
if not np.isnan(feature_arr[i]):
signal_values.append(feature_arr[i])
return result
BP-04:数据范围不匹配(中等)
症状:对于数据较早的资产(BNB、XRP),SQL 和 Python 的信号数量不同。
根本原因:load_range_bars() 默认使用 start='2020-01-01',但 SQL 没有下限。
修复:始终传入 start='2017-01-01',以涵盖所有可用数据。
BP-05:重叠持仓导致保证金耗尽(中等)
症状:因保证金不足而取消订单。交易数少于预期。
根本原因:使用 hedging=True 和默认的全权益仓位规模时,重叠持仓会耗尽可用保证金。
修复:使用固定比例仓位规模:
self.buy(size=0.01) # 1% equity per trade
BP-06:信号时间戳与入场时间戳(低)
症状:关卡 2(时间戳匹配)未通过,因为 SQL 使用信号柱时间戳,而 Python 使用入场柱时间戳。
根本原因:SQL 输出信号检测柱的 timestamp_ms。Python 的 EntryTime 是成交柱(信号之后的下一根柱)。二者相差 1 根柱。
修复:在策略的 next() 方法中记录信号柱时间戳:
# Before calling self.buy()
self._signal_timestamps.append(int(self.data.index[-1].timestamp() * 1000))
5 道门控的 Oracle 验证框架
| 门控 | 指标 | 阈值 | 检测内容 |
|---|---|---|---|
| 1 | 信号数量 | <5% 差异 | 信号缺失、筛选条件不一致 |
| 2 | 时间戳匹配 | >95% | 时间偏移、预热差异 |
| 3 | 入场价格 | >95% | 价格来源不匹配、排序顺序 |
| 4 | 退出类型 | >90% | 屏障逻辑差异 |
| 5 | Kelly 比例 | <0.02 | 汇总结果一致性 |
预期残余差异:在 TIME 屏障边界(第 50 根柱线)处,每种资产有 1–2 个退出类型不匹配。SQL 使用 fwd_closes[max_bars],backtesting.py 在当前柱线价格处平仓。对 Kelly 的影响 < 0.006。
策略架构:单持仓与多持仓
| 模式 | 构造函数 | 用例 | 仓位规模 |
|---|---|---|---|
| 单持仓 | hedging=False(默认) | Champion 持仓 1 根柱线 | 全部权益 |
| 多持仓 | hedging=True, exclusive_orders=False | SQL Oracle 验证 | 固定比例(size=0.01) |
多持仓策略模板
class Gen600Strategy(Strategy):
def next(self):
current_bar = len(self.data) - 1
# 1. Register newly filled trades and set barriers
for trade in self.trades:
tid = id(trade)
if tid not in self._known_trades:
self._known_trades.add(tid)
self._trade_entry_bar[tid] = current_bar
actual_entry = trade.entry_price
if self.tp_mult > 0:
trade.tp = actual_entry * (1.0 + self.tp_mult * self.threshold_pct)
if self.sl_mult > 0:
trade.sl = actual_entry * (1.0 - self.sl_mult * self.threshold_pct)
# 2. Check time barrier for each open trade
for trade in list(self.trades):
tid = id(trade)
entry_bar = self._trade_entry_bar.get(tid, current_bar)
if self.max_bars > 0 and (current_bar - entry_bar) >= self.max_bars:
trade.close()
self._trade_entry_bar.pop(tid, None)
# 3. Check for new signal (no position guard — overlapping allowed)
if self._is_signal[current_bar]:
self.buy(size=0.01)
数据加载
from data_loader import load_range_bars
df = load_range_bars(
symbol="SOLUSDT",
threshold=1000,
start="2017-01-01", # Cover all available data
end="2025-02-05", # Match SQL cutoff
extra_columns=["volume_per_trade", "lookback_price_range"], # Gen600 features
)
HTML 权益曲线图:缩放时自动适配 Y 轴(BP-07 至 BP-10)
backtesting.py 通过 bt.plot() 生成 Bokeh HTML 图表。默认情况下,Y 轴是固定的——缩放 X 轴不会重新缩放 Y 轴以适配可见数据。因此,无法检查跨越 4 个以上数量级的权益曲线的放大区域。
参考实现:example-org/research-repo 中的 scripts/gen800/plotting.py。
BP-07:切勿对权益使用 LogScale(关键)
症状:缩放时权益面板的 Y 轴不会自动适配。JS 回调会静默失败。
根本原因:在多面板且共享链接 x_range 的布局中,Bokeh 的 LogScale 会破坏 CustomJS Y 轴范围回调。js_on_change 回调会触发,但 LogScale 渲染器会忽略对 y_range.start/y_range.end 的赋值。
通过概念验证确认:LogScale 与 JS 回调在单面板模式下有效,但在 5 个面板共享链接 x_range 时会失败。
修复方法:在 Python 中将权益数据转换为 log10(),在线性比例上显示,并使用 CustomJSTickFormatter 将 10^tick 显示为易读的数值:
import numpy as np
from bokeh.models import CustomJSTickFormatter, Range1d
# Transform data
raw = np.asarray(src.data["equity"], dtype=float)
raw = np.where(raw > 0, raw, 1e-10)
src.data["equity"] = np.log10(raw).tolist()
# Custom tick formatter (shows 1%, 100%, 10K%, 1M%)
child.yaxis[0].formatter = CustomJSTickFormatter(code="""
const v = Math.pow(10, tick);
if (v >= 1e6) return (v/1e6).toFixed(1) + 'M%';
if (v >= 1e3) return (v/1e3).toFixed(0) + 'K%';
if (v >= 1) return v.toFixed(0) + '%';
return v.toFixed(2) + '%';
""")
# MUST set initial y_range explicitly (backtesting.py leaves it as NaN)
valid = eq[np.isfinite(eq)]
pad = (valid.max() - valid.min()) * 0.05
child.y_range = Range1d(start=valid.min() - pad, end=valid.max() + pad)
BP-08:按面板匹配列(关键)
症状:缩放时,回撤/盈亏面板变为空白。显示数百万百分比。
根本原因:多个面板共享同一个 ColumnDataSource(backtesting.py 的优化方式)。权益面板的 Line 渲染器和回撤面板的 Line 渲染器都引用了包含 equity、drawdown、High、Low 等字段的数据源。简单地“查找第一个 y 列”的方法会为回撤面板选中 equity。
修复方法:先按 panel_label 匹配,再按列名匹配:
panel_label = child.yaxis[0].axis_label or ""
# Label-first matching (panels share data sources!)
if "Drawdown" in panel_label and "drawdown" in d:
hi_col = lo_col = "drawdown"
elif "Equity" in panel_label and "equity" in d:
hi_col = lo_col = "equity"
elif "High" in d and "Low" in d: # OHLC
hi_col, lo_col = "High", "Low"
BP-09:Y 轴自动适配的 JS 回调模式(已验证)
为每个面板的 x_range.js_on_change 附加一个 CustomJS 回调。回调会扫描可见数据并直接设置 y_range:
from bokeh.models import CustomJS
cb = CustomJS(
args={"source": src, "yr": child.y_range,
"x_col": "index", "y_col": "equity"},
code="""
const xs = source.data[x_col];
const ys = source.data[y_col];
const x0 = cb_obj.start, x1 = cb_obj.end;
let lo = Infinity, hi = -Infinity;
for (let i = 0; i < xs.length; i++) {
if (xs[i] >= x0 && xs[i] <= x1 && isFinite(ys[i])) {
if (ys[i] < lo) lo = ys[i];
if (ys[i] > hi) hi = ys[i];
}
}
if (isFinite(lo) && isFinite(hi) && lo < hi) {
const pad = (hi - lo) * 0.05 || 0.001;
yr.start = lo - pad;
yr.end = hi + pad;
}
""",
)
child.x_range.js_on_change("start", cb)
child.x_range.js_on_change("end", cb)
BP-10:DataRange1d(only_visible=True) 不可靠
症状:DataRange1d(only_visible=True) 在简单情况下有效,但在以下场景会失败:
- LogScale 面板(Y 轴范围在错误的空间中计算)
- VBar 渲染器(蜡烛图——边界跟踪不正确)
- 面板间共享数据源
修复方法:始终使用 JS 回调模式(BP-09),不要使用 DataRange1d(only_visible=True)。
面板数据源参考(backtesting.py 内部实现)
| 面板 | 标签 | 渲染器 | Y 列 | 来源 |
|---|---|---|---|---|
| 权益 | "Equity" | Patch (equity_dd)、Line (equity) | equity | 共享 OHLC 数据源 |
| 回撤 | "Drawdown" | Line (drawdown)、Scatter(峰值) | drawdown | 共享 OHLC 数据源 |
| 盈亏 | "Profit / Loss" | Scatter (returns)、MultiLine | y 或 returns | 独立交易数据源 |
| OHLC | ""(无标签) | Segment、VBar | High/Low | 共享 OHLC 数据源 |
| 成交量 | "Volume" | VBar | Volume(顶部) | 共享 OHLC 数据源 |
项目文件(rangebar-patterns 仓库)
| 文件 | 路径 |
|---|---|
| Oracle 对比脚本 | scripts/gen600_oracle_compare.py |
| Gen600 策略(参考) | backtest/backtesting_py/gen600_strategy.py |
| SQL Oracle 查询模板 | sql/gen600_oracle_trades.sql |
| Oracle 验证结果 | findings/2026-02-12-gen600-oracle-validation.md |
| 回测 CLAUDE.md | backtest/CLAUDE.md |
| ClickHouse AP-16 | .claude/skills/clickhouse-antipatterns/SKILL.md |
| 分支源码 | ~/fork-tools/backtesting.py/ |
执行后反思
此技能完成后,关闭前请检查:
- 命令是否成功?——如果没有,修正导致失败的指令或错误表。
- 参数或输出是否发生变化?——如果底层工具的接口发生变化,更新用法示例和参数表,使其保持一致。
- 是否需要变通处理?——如果你不得不临时调整(使用不同的标志、增加额外步骤),请更新此 SKILL.md,避免下次调用时重复采用相同的变通方案。
仅当问题真实且可复现时才更新,不要根据猜测进行更新。