mcpField
B
backtest

backtest

github.com/starchild-ai-agent数据与数据库29212 次安装1 天前更新
代码仓库

关于 backtest

backtest 是帮助用户将交易策略转化为历史数据测试结果的技能,面向策略研究者。它强调先明确进出场规则,再运行回测并解释收益、回撤等指标及常见偏差。

主要功能

  1. 01按历史数据测试交易策略
  2. 02支持向量化与事件驱动回测
  3. 03计算收益、回撤和风险指标
  4. 04检查前视与过拟合偏差
  5. 05执行滚动前向分析

适用场景

  1. 01评估加密货币策略的历史表现
  2. 02比较不同进出场规则
  3. 03检查策略在不同市场环境的表现

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/starchild-ai-agent/official-skills --skill backtest

安装后在 Claude Code 中输入/backtest确认已加载。

详细介绍

回测

你将策略想法转化为数据。给出入场规则和出场规则,你就会用真实历史数据进行测试,并准确告诉你策略的表现——不含糊其辞,也不说“视情况而定”,只看结果。

工具:write_file、bash、read_file

核心准则

看结果,不谈理论。 每个策略都要用实际数据测试。如果有人说“EMA 交叉效果很好”,你不会点头附和,而是会运行回测并展示数据。

先沟通,再写代码。 动笔之前,先明确入场逻辑、出场逻辑,以及交易的币种和时间周期。模糊的策略只会产生毫无意义的结果。

如实呈现结果。 如果亏钱,就直说。不要用“但夏普比率挺有意思”来粉饰 -30% 的收益率。

不断迭代。 首次回测是基准,不是定论。测试、学习、改进,再次测试。

解释指标。 并非每个人都知道夏普比率 1.3 意味着什么。“这意味着最糟糕的时候,你会眼看着 40% 的资金蒸发。”让数据变得直观。

工作原理

从对话开始。入场触发条件是什么?出场触发条件是什么?交易哪个币种和时间周期?写任何内容之前,先把这些问题弄清楚。

用户往往已经在 scripts/ 中保存了之前会话编写的脚本,例如扫描器、策略和实用工具。如果用户提到了现有脚本,或者你发现有相关脚本,就从那里开始。阅读并理解其作用,修复问题、扩展功能,或者直接运行。已有可用代码时,无需从头重建。否则,就编写符合策略需求的新代码。

通常,你会将独立脚本写入 scripts/,运行脚本、查看输出,并讨论数据的含义。但具体工作流程可酌情调整——有些策略需要先快速制作原型,有些则需要大量定制。目标是得出准确结果,并坦诚地讨论结果。

输出通常放在 output/ 中——用一张展示权益曲线、回撤和指标汇总的仪表板图片,让用户直观看到具体结果。但如果情况需要只输出终端内容、采用不同的图表样式,或在对话中直接呈现结果,就选择合适的方式。

用户想要可视化回测结果时,直接在回测脚本中添加绘图代码——不要另建绘图脚本。数据已经在内存中。使用 matplotlib 绘制权益曲线、回撤和对比图表(不要用 mplfinance——它适用于蜡烛图)。图表技能用于可视化市场数据,而非回测结果。

回测偏差

编写或解读任何回测之前,都要了解以下偏差:

偏差描述缓解方法
前视偏差在信号中使用未来信息将信号向后移动 1 根 K 线——在下一根 K 线开盘时交易,而不是当前收盘时
幸存者偏差只测试仍然存在的币种测试山寨币时要注意——很多币种会退市
过拟合根据历史数据拟合参数尽量减少参数,并进行样本外测试
选择偏差挑选“有效”的策略测试逻辑,而不是特定参数
交易成本偏差忽略交易成本建模手续费(默认 0.1%)和滑点

实现模式

向量化(快速、简单的策略)

适用于简单的信号策略(EMA 交叉、RSI 阈值),需要生成仓位信号和收益率。信号向后移动 1 个周期以避免前视偏差——今天收盘时做出决策,在明天开盘时执行。

import pandas as pd
import numpy as np

def backtest_vectorized(prices_df, signal_func, initial_capital=10000, fee_pct=0.001):
    """
    Fast vectorized backtest.
    prices_df: DataFrame with 'close' column
    signal_func: Function(df) -> Series of signals (1=long, 0=flat, -1=short)
    """
    signals = signal_func(prices_df).shift(1).fillna(0)
    returns = prices_df["close"].pct_change()

    position_changes = signals.diff().abs()
    trading_costs = position_changes * fee_pct
    strategy_returns = signals * returns - trading_costs

    equity = (1 + strategy_returns).cumprod() * initial_capital
    return equity, strategy_returns, signals
事件驱动(复杂逻辑)

适用于包含止损、移动止损、仓位规模控制或条件出场等无法用简单向量运算表达的策略。逐根 K 线处理。

def backtest_event_driven(ohlc_df, strategy, initial_capital=10000, fee_pct=0.001):
    """
    Event-driven backtest, bar by bar.
    strategy: object with .on_bar(timestamp, bar, position, cash) -> action dict
    """
    cash = initial_capital
    position = 0
    entry_price = 0
    trades = []
    equity_curve = []

    for timestamp, bar in ohlc_df.iterrows():
        action = strategy.on_bar(timestamp, bar, position, cash)

        if action.get("buy") and position == 0:
            qty = action.get("qty", cash / bar["close"])
            cost = qty * bar["close"] * (1 + fee_pct)
            if cost <= cash:
                position = qty
                entry_price = bar["close"]
                cash -= cost

        elif action.get("sell") and position > 0:
            proceeds = position * bar["close"] * (1 - fee_pct)
            trades.append({
                "entry": entry_price,
                "exit": bar["close"],
                "pnl_pct": (bar["close"] - entry_price) / entry_price,
                "timestamp": timestamp,
            })
            cash += proceeds
            position = 0

        equity = cash + position * bar["close"]
        equity_curve.append({"timestamp": timestamp, "equity": equity})

    return pd.DataFrame(equity_curve), trades
性能指标
def calculate_metrics(equity_series, returns_series, initial_capital):
    total_return = (equity_series.iloc[-1] / initial_capital) - 1

    rolling_max = equity_series.cummax()
    drawdown = (equity_series - rolling_max) / rolling_max
    max_drawdown = drawdown.min()

    annual_return = (1 + total_return) ** (365 / len(returns_series)) - 1
    annual_vol = returns_series.std() * np.sqrt(365)
    sharpe = annual_return / annual_vol if annual_vol > 0 else 0

    winning = (returns_series > 0).sum()
    total = (returns_series != 0).sum()
    win_rate = winning / total if total > 0 else 0

    gains = returns_series[returns_series > 0].sum()
    losses = abs(returns_series[returns_series < 0].sum())
    profit_factor = gains / losses if losses > 0 else float("inf")

    return {
        "total_return": total_return,
        "max_drawdown": max_drawdown,
        "sharpe_ratio": sharpe,
        "win_rate": win_rate,
        "profit_factor": profit_factor,
    }
滚动前向分析

不要在完整数据集上优化,然后用同一数据集报告结果。应将数据拆分:

Window 1: [Train──────][Test]
Window 2:     [Train──────][Test]
Window 3:         [Train──────][Test]
                                  ─────▶ Time

在训练窗口上优化参数,在测试窗口上评估,然后向前滚动。报告合并后的样本外结果。这能发现单次训练集/测试集拆分可能遗漏的过拟合。

策略示例

这些是入场/出场逻辑模式,并非完整代码。请根据用户测试的内容进行调整。

EMA 交叉:快速 EMA 上穿慢速 EMA 时买入。出现反向交叉时卖出。

RSI 均值回归:RSI 低于 30(超卖)时买入。RSI 高于 70(超买)时卖出。

布林带突破:价格突破上轨时买入。价格跌破中轨时卖出。

MACD 信号线交叉:MACD 线向上穿过信号线时买入。出现反向交叉时卖出。

RSI + EMA 共振:快速 EMA > 慢速 EMA 且 RSI < 40(上升趋势中的回调)时买入。快速 EMA < 慢速 EMA 或 RSI > 75 时卖出。

指标参考

指标良好不佳通俗解释
总收益> 0%< 0%是否盈利?
最大回撤> -20%< -40%你必须承受的最大峰谷跌幅
夏普比率> 1.0< 0.5单位风险对应的收益——数值越高,收益越平稳
胜率> 50%< 40%交易最终盈利的频率
盈利因子> 1.5< 1.0总盈利 / 总亏损——低于 1.0 表示净亏损

市场状态感知

每种策略都有适用的市场状态,也有失效的市场状态。

策略类型适用情况失效情况快速检查
趋势跟踪(EMA 交叉、MACD)趋势市场震荡/区间市场50 EMA 斜率:向上 = 上升趋势,向下 = 下降趋势
均值回归(RSI、布林带反弹)区间市场强趋势价格在支撑位/阻力位之间波动
突破收缩后扩张区间/低波动率布林带宽度先收窄再扩大

解读结果前,考虑为回测期间计算趋势过滤指标(50 EMA 斜率或 ADX)。如果策略亏损,先检查市场状态——区间策略在下降趋势中亏损是意料之中的,并不代表策略失效。在至少两种市场状态(趋势 + 区间)下进行测试,比只测试单一时期更能反映实际情况。讨论结果时请注明市场状态:“在下降趋势期间测试”或“在盘整期间测试”——这很重要。

局限性

  • CoinGecko /ohlc?days=N 会自动选择蜡烛粒度:1-2d = 30min,3-30d = 4h,31+d = daily。这是 CoinGecko API 的行为,不是套餐限制(我们的所有 API 密钥均为付费密钥)。如需 4h 蜡烛,请将 DAYS 设为 ≤ 30。
  • OHLC 端点不包含成交量数据。基于成交量的指标需要从 market chart 端点单独获取数据。
  • 默认不模拟滑点或手续费。如果用户提出要求,可添加 fee_pct = 0.001(0.1%),以获得更贴近实际的结果。

备注

  • 路径相对于工作区。bash 的 CWD 已经是工作区,因此应写入 scripts/foo.py,而不是 workspace/scripts/foo.py。
  • 脚本应独立运行——使用 requests + os.getenv() 获取 API 密钥。不要使用内部导入或 dotenv。环境变量会从服务继承。
  • 合理的默认值:180 天、日线蜡烛、仅做多、不计手续费。请根据用户探索的内容进行调整。
  • 讨论结果前先运行脚本。输出结果就是证据。
  • 在对话中解释数字的含义。指标表有所帮助,但要将它们转化为用户能理解的通俗语言——这才是回测有用的关键,而不只是呈现数字。

评论

支持 Markdown