用 Python + backtrader 做专业级策略回测

用户头像sh_*2176oo
2026-06-15 发布

用 Python + backtrader 做专业级策略回测

前面几篇文章里,我们用 pandas 手写了回测逻辑:计算信号、乘以收益率、累乘得到净值曲线。这种方式简单直接,适合快速验证一个想法。

但当你的策略开始变复杂——有止损逻辑、有仓位管理、有多标的轮动、有交易费用——纯手写的回测代码就会变得臃肿且容易出错。

这时候你需要一个回测框架。backtrader 是 Python 量化圈最常用的回测框架之一:它支持事件驱动、仓位管理、交易成本、多标的、多时间周期,而且社区生态比较成熟。

这篇文章的目标是:用 AlphaFeed 拿数据,喂给 backtrader 跑回测。两者各做自己擅长的事——AlphaFeed 管数据获取,backtrader 管回测引擎。

1. 安装依赖

pip install alphafeed backtrader pandas matplotlib

2. 把 AlphaFeed 数据转成 backtrader 格式

backtrader 有自己的数据格式要求。最常用的方式是通过 bt.feeds.PandasData 把 DataFrame 喂进去。

AlphaFeed 返回的 DataFrame 字段名和 backtrader 默认的不完全一样,所以需要做一层映射:

import pandas as pd
import backtrader as bt
from alphafeed import AlphaFeed

def load_alphafeed_data(symbol: str, count: int = 500, period: str = "1d") -> bt.feeds.PandasData:
    af = AlphaFeed()

    df = af.klines.get(
        symbol,
        period=period,
        count=count,
        adjust="forward",
        to_dataframe=True,
    )
    df = df.sort_values("trade_date").reset_index(drop=True)

    df["trade_date"] = pd.to_datetime(df["trade_date"])
    df = df.set_index("trade_date")

    df = df.rename(columns={
        "open": "open",
        "high": "high",
        "low": "low",
        "close": "close",
        "volume": "volume",
    })

    data = bt.feeds.PandasData(
        dataname=df,
        open="open",
        high="high",
        low="low",
        close="close",
        volume="volume",
        openinterest=-1,
    )

    return data

这个函数封装了"从 AlphaFeed 获取数据 → 转成 backtrader 能识别的格式"的全过程。后面所有策略都可以复用它。

3. 第一个 backtrader 策略:双均线交叉

我们先用 backtrader 重写第 01 篇里的双均线策略,看看框架化之后的写法:

import backtrader as bt

class DualMA(bt.Strategy):
    params = (
        ("short_period", 20),
        ("long_period", 60),
    )

    def __init__(self):
        self.ma_short = bt.indicators.SimpleMovingAverage(
            self.data.close, period=self.params.short_period,
        )
        self.ma_long = bt.indicators.SimpleMovingAverage(
            self.data.close, period=self.params.long_period,
        )
        self.crossover = bt.indicators.CrossOver(self.ma_short, self.ma_long)

    def next(self):
        if not self.position:
            if self.crossover > 0:
                self.buy()
        elif self.crossover < 0:
            self.sell()

和手写 pandas 回测相比,backtrader 的好处是:信号判断和仓位管理分开了。你不需要手动维护 position 列,不需要手动做 shift(1) 来避免未来函数——框架的事件驱动机制自动保证了这一点。

4. 把数据和策略组装起来跑回测

import backtrader as bt
import pandas as pd
from alphafeed import AlphaFeed

def load_alphafeed_data(symbol: str, count: int = 500) -> bt.feeds.PandasData:
    af = AlphaFeed()
    df = af.klines.get(
        symbol, period="1d", count=count,
        adjust="forward", to_dataframe=True,
    )
    df = df.sort_values("trade_date").reset_index(drop=True)
    df["trade_date"] = pd.to_datetime(df["trade_date"])
    df = df.set_index("trade_date")
    return bt.feeds.PandasData(
        dataname=df,
        open="open", high="high", low="low",
        close="close", volume="volume", openinterest=-1,
    )

class DualMA(bt.Strategy):
    params = (
        ("short_period", 20),
        ("long_period", 60),
    )

    def __init__(self):
        self.ma_short = bt.indicators.SMA(self.data.close, period=self.p.short_period)
        self.ma_long = bt.indicators.SMA(self.data.close, period=self.p.long_period)
        self.crossover = bt.indicators.CrossOver(self.ma_short, self.ma_long)

    def next(self):
        if not self.position:
            if self.crossover > 0:
                self.buy()
        elif self.crossover < 0:
            self.sell()


cerebro = bt.Cerebro()

data = load_alphafeed_data("600519.SH", count=800)
cerebro.adddata(data)

cerebro.addstrategy(DualMA)

cerebro.broker.setcash(1_000_000)
cerebro.broker.setcommission(commission=0.001)

cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name="sharpe", riskfreerate=0.02)
cerebro.addanalyzer(bt.analyzers.DrawDown, _name="drawdown")
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name="trades")

print(f"初始资金: {cerebro.broker.getvalue():,.0f}")

results = cerebro.run()
strategy = results[0]

print(f"最终资金: {cerebro.broker.getvalue():,.0f}")
print(f"总收益率: {(cerebro.broker.getvalue() / 1_000_000 - 1) * 100:.2f}%")

sharpe = strategy.analyzers.sharpe.get_analysis()
dd = strategy.analyzers.drawdown.get_analysis()
trades = strategy.analyzers.trades.get_analysis()

print(f"夏普比率: {sharpe.get('sharperatio', 'N/A')}")
print(f"最大回撤: {dd.max.drawdown:.2f}%")
print(f"总交易次数: {trades.total.total}")

cerebro.plot(style="candlestick")

backtrader 自带的 cerebro.plot() 会画出包含 K 线、均线、买卖点标记、资金曲线的完整图表。

5. 加入止损和止盈

纯均线策略没有风控,一旦趋势反转就会坐过山车。加上止损止盈:

import backtrader as bt

class DualMAWithRisk(bt.Strategy):
    params = (
        ("short_period", 20),
        ("long_period", 60),
        ("stop_loss", 0.05),
        ("take_profit", 0.15),
    )

    def __init__(self):
        self.ma_short = bt.indicators.SMA(self.data.close, period=self.p.short_period)
        self.ma_long = bt.indicators.SMA(self.data.close, period=self.p.long_period)
        self.crossover = bt.indicators.CrossOver(self.ma_short, self.ma_long)
        self.entry_price = None

    def next(self):
        if not self.position:
            if self.crossover > 0:
                self.buy()
                self.entry_price = self.data.close[0]
        else:
            current = self.data.close[0]
            pnl_pct = (current - self.entry_price) / self.entry_price

            if pnl_pct <= -self.p.stop_loss:
                self.sell()
                self.log(f"止损平仓 | 亏损 {pnl_pct:.2%}")
            elif pnl_pct >= self.p.take_profit:
                self.sell()
                self.log(f"止盈平仓 | 盈利 {pnl_pct:.2%}")
            elif self.crossover < 0:
                self.sell()
                self.log(f"信号平仓 | 盈亏 {pnl_pct:.2%}")

    def log(self, txt):
        dt = self.data.datetime.date(0)
        print(f"[{dt}] {txt}")

这段代码展示了 backtrader 的一个核心优势:你可以在 next() 方法里自由地写交易逻辑,而不需要像 pandas 回测那样用向量化操作来模拟条件判断。

6. 参数优化

backtrader 支持内置的参数优化(grid search):

import backtrader as bt

cerebro = bt.Cerebro()

data = load_alphafeed_data("600519.SH", count=800)
cerebro.adddata(data)

cerebro.optstrategy(
    DualMA,
    short_period=range(10, 31, 5),
    long_period=range(40, 81, 10),
)

cerebro.broker.setcash(1_000_000)
cerebro.broker.setcommission(commission=0.001)
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name="sharpe")

results = cerebro.run()

param_results = []
for run in results:
    for strategy in run:
        sharpe_info = strategy.analyzers.sharpe.get_analysis()
        sr = sharpe_info.get("sharperatio", None)
        param_results.append({
            "short": strategy.params.short_period,
            "long": strategy.params.long_period,
            "sharpe": sr,
            "final_value": strategy.broker.getvalue(),
        })

import pandas as pd
param_df = pd.DataFrame(param_results).sort_values("sharpe", ascending=False, na_position="last")
print("=== 参数优化结果 ===")
print(param_df.head(10).to_string(index=False))

注意:参数优化的结果仅供参考。夏普最高的参数组合可能是过拟合的。你应该把数据分成训练集和测试集,在训练集上选参数,在测试集上验证。

7. 多标的回测

backtrader 支持同时加载多只股票的数据,在一个策略里交叉操作:

import backtrader as bt
import pandas as pd
from alphafeed import AlphaFeed

def load_data(symbol: str, count: int = 500) -> bt.feeds.PandasData:
    af = AlphaFeed()
    df = af.klines.get(
        symbol, period="1d", count=count,
        adjust="forward", to_dataframe=True,
    )
    df = df.sort_values("trade_date").reset_index(drop=True)
    df["trade_date"] = pd.to_datetime(df["trade_date"])
    df = df.set_index("trade_date")
    return bt.feeds.PandasData(
        dataname=df,
        open="open", high="high", low="low",
        close="close", volume="volume", openinterest=-1,
    )


class MultiStockMomentum(bt.Strategy):
    params = (
        ("momentum_period", 20),
        ("rebalance_interval", 20),
        ("top_n", 2),
    )

    def __init__(self):
        self.counter = 0
        self.momentums = {}
        for d in self.datas:
            self.momentums[d._name] = bt.indicators.RateOfChange(
                d.close, period=self.p.momentum_period,
            )

    def next(self):
        self.counter += 1
        if self.counter % self.p.rebalance_interval != 0:
            return

        rankings = []
        for d in self.datas:
            mom = self.momentums[d._name][0]
            rankings.append((d, mom))

        rankings.sort(key=lambda x: x[1], reverse=True)

        top_stocks = set(d._name for d, _ in rankings[:self.p.top_n])

        for d in self.datas:
            if d._name in top_stocks:
                target_pct = 1.0 / self.p.top_n
                self.order_target_percent(d, target=target_pct)
            else:
                if self.getposition(d).size > 0:
                    self.close(d)


cerebro = bt.Cerebro()

symbols = {
    "maotai": "600519.SH",
    "pingan": "601318.SH",
    "byd": "002594.SZ",
    "catl": "300750.SZ",
    "wuliangye": "000858.SZ",
}

for name, sym in symbols.items():
    data = load_data(sym, count=500)
    data._name = name
    cerebro.adddata(data, name=name)

cerebro.addstrategy(MultiStockMomentum)
cerebro.broker.setcash(1_000_000)
cerebro.broker.setcommission(commission=0.001)

cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name="sharpe")
cerebro.addanalyzer(bt.analyzers.DrawDown, _name="drawdown")

print(f"初始资金: {cerebro.broker.getvalue():,.0f}")

results = cerebro.run()
st = results[0]

print(f"最终资金: {cerebro.broker.getvalue():,.0f}")
print(f"总收益率: {(cerebro.broker.getvalue() / 1_000_000 - 1) * 100:.2f}%")

sharpe = st.analyzers.sharpe.get_analysis()
dd = st.analyzers.drawdown.get_analysis()
print(f"夏普比率: {sharpe.get('sharperatio', 'N/A')}")
print(f"最大回撤: {dd.max.drawdown:.2f}%")

这个策略每 20 天做一次再平衡:计算所有标的的 20 日动量,选动量最强的 2 只等权持有,其余清仓。

8. 自定义 AlphaFeed 数据源类

如果你经常用 AlphaFeed + backtrader,可以封装一个更方便的数据源类:

import pandas as pd
import backtrader as bt
from alphafeed import AlphaFeed


class AlphaFeedData(bt.feeds.PandasData):
    """AlphaFeed 数据源封装,直接传 symbol 即可"""

    @classmethod
    def create(
        cls,
        symbol: str,
        period: str = "1d",
        count: int = 500,
        adjust: str = "forward",
        api_key: str = None,
    ):
        af = AlphaFeed(api_key=api_key) if api_key else AlphaFeed()

        df = af.klines.get(
            symbol,
            period=period,
            count=count,
            adjust=adjust,
            to_dataframe=True,
        )
        df = df.sort_values("trade_date").reset_index(drop=True)
        df["trade_date"] = pd.to_datetime(df["trade_date"])
        df = df.set_index("trade_date")

        return cls(
            dataname=df,
            open="open",
            high="high",
            low="low",
            close="close",
            volume="volume",
            openinterest=-1,
        )


cerebro = bt.Cerebro()
cerebro.adddata(AlphaFeedData.create("600519.SH", count=800))
cerebro.addstrategy(DualMA)
cerebro.broker.setcash(1_000_000)
cerebro.run()

一行 AlphaFeedData.create("600519.SH") 就搞定数据加载,后续所有策略都可以直接用。

9. 回测结果的完整报告

backtrader 内置了丰富的分析器,可以生成专业级的回测报告:

import backtrader as bt

cerebro = bt.Cerebro()

data = load_alphafeed_data("600519.SH", count=800)
cerebro.adddata(data)
cerebro.addstrategy(DualMAWithRisk, stop_loss=0.05, take_profit=0.15)

cerebro.broker.setcash(1_000_000)
cerebro.broker.setcommission(commission=0.001)

cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name="sharpe", riskfreerate=0.02)
cerebro.addanalyzer(bt.analyzers.DrawDown, _name="drawdown")
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name="trades")
cerebro.addanalyzer(bt.analyzers.Returns, _name="returns")
cerebro.addanalyzer(bt.analyzers.VWR, _name="vwr")

results = cerebro.run()
st = results[0]

print("=" * 50)
print("回测报告")
print("=" * 50)

returns = st.analyzers.returns.get_analysis()
print(f"总收益率:   {returns.get('rtot', 0) * 100:.2f}%")
print(f"年化收益率: {returns.get('rnorm', 0) * 100:.2f}%")

sharpe = st.analyzers.sharpe.get_analysis()
print(f"夏普比率:   {sharpe.get('sharperatio', 'N/A')}")

vwr = st.analyzers.vwr.get_analysis()
print(f"变异加权收益: {vwr.get('vwr', 'N/A')}")

dd = st.analyzers.drawdown.get_analysis()
print(f"最大回撤:   {dd.max.drawdown:.2f}%")
print(f"最长回撤期: {dd.max.len} 天")

trades = st.analyzers.trades.get_analysis()
print(f"\n交易统计:")
print(f"  总交易: {trades.total.total}")
if hasattr(trades, 'won') and hasattr(trades.won, 'total'):
    print(f"  盈利: {trades.won.total}")
    print(f"  亏损: {trades.lost.total}")
    win_rate = trades.won.total / trades.total.total * 100 if trades.total.total > 0 else 0
    print(f"  胜率: {win_rate:.1f}%")

10. pandas 手写 vs backtrader:怎么选

维度 pandas 手写 backtrader
上手速度 快,几十行就能跑 需要学框架概念
适合场景 快速验证想法、因子研究 完整策略开发、仓位管理
止损/止盈 手写复杂 内置支持
多标的 需要自己管理 框架原生支持
交易费用 手动扣减 配置一次自动计算
参数优化 自己写循环 内置optstrategy
可视化 自己画 matplotlib cerebro.plot() 一键出图
实盘对接 支持(需要写 broker 适配)

建议的路线是:先用 pandas 快速验证 10 个想法,选出 2–3 个有潜力的,再用 backtrader 做完整回测和风控优化。

结语

AlphaFeed 和 backtrader 是"数据层"和"回测层"的自然搭配。

AlphaFeed 把数据获取压缩到一行代码,backtrader 把回测逻辑组织成专业框架。两者组合起来,你可以快速迭代策略、精确计算交易成本、做参数优化、跑多标的轮动——而不需要从零造轮子。

这篇文章提供了一个完整的"数据 → 策略 → 回测 → 报告"流程模板。你可以在此基础上替换策略逻辑、增加标的数量、调整风控参数,逐步构建自己的策略库。

参考链接:

评论