mcpField
B
backtesting-py-oracle

backtesting-py-oracle

github.com/terrylica数据与数据库2133 次安装1 天前更新
代码仓库

关于 backtesting-py-oracle

为使用 backtesting.py 的量化研究者提供验证 ClickHouse SQL 扫描结果的配置与错误规避指导。重点处理重叠交易、数据对齐和图表缩放等问题,并以五项指标核对 Python 与 SQL 结果。

主要功能

  1. 01配置多持仓回测以匹配 SQL
  2. 02识别交易排序与时间戳差异
  3. 03处理滚动分位数中的 NaN
  4. 04按五项指标验证结果
  5. 05修正缩放时的图表纵轴

适用场景

  1. 01核对 SQL 与 Python 的交易数量
  2. 02诊断入场价格或时间不一致
  3. 03检查跨资产信号过滤偏差
  4. 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%屏障逻辑差异
5Kelly 比例<0.02汇总结果一致性

预期残余差异:在 TIME 屏障边界(第 50 根柱线)处,每种资产有 1–2 个退出类型不匹配。SQL 使用 fwd_closes[max_bars],backtesting.py 在当前柱线价格处平仓。对 Kelly 的影响 < 0.006。


策略架构:单持仓与多持仓

模式构造函数用例仓位规模
单持仓hedging=False(默认)Champion 持仓 1 根柱线全部权益
多持仓hedging=True, exclusive_orders=FalseSQL 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)、MultiLiney 或 returns独立交易数据源
OHLC""(无标签)Segment、VBarHigh/Low共享 OHLC 数据源
成交量"Volume"VBarVolume(顶部)共享 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.mdbacktest/CLAUDE.md
ClickHouse AP-16.claude/skills/clickhouse-antipatterns/SKILL.md
分支源码~/fork-tools/backtesting.py/

执行后反思

此技能完成后,关闭前请检查:

  1. 命令是否成功?——如果没有,修正导致失败的指令或错误表。
  2. 参数或输出是否发生变化?——如果底层工具的接口发生变化,更新用法示例和参数表,使其保持一致。
  3. 是否需要变通处理?——如果你不得不临时调整(使用不同的标志、增加额外步骤),请更新此 SKILL.md,避免下次调用时重复采用相同的变通方案。

仅当问题真实且可复现时才更新,不要根据猜测进行更新。

评论

支持 Markdown