量化回测从 0 到 1 搭建路径图:用 Backtrader 实现"策略温度计",3 步完成回测系统(完整代码)

5 阅读1分钟

声明:本文所有代码仅供学习参考,不构成任何投资建议。量化交易有风险,入市需谨慎。

引言:为什么你的回测系统总是"差一点"?

很多量化新手都有过类似经历:

  • 回测结果挺好,一实盘就亏钱
  • 不知道策略什么时候有效,什么时候失效
  • 就像有一把"温度计",能随时显示策略的"体温"就好了

"策略温度计"的核心思路:

  • 温度高(策略过热)→ 减少仓位或暂停
  • 温度适中(策略正常)→ 保持正常仓位
  • 温度低(策略遇冷)→ 可能存在机会

本文带你用 Backtrader 从 0 到 1 搭建一个带"温度计"功能的回测系统,3 步实现完整的策略回测框架。

一、Backtrader 核心概念:5 个必须理解的组件

组件作用生活化比喻
Cerebro引擎大脑,协调整个回测流程"公司 CEO"
Strategy交易策略逻辑"决策者"
Data Feeds历史行情数据"原材料"
Broker模拟交易所执行交易"交易员"
Analyzers绩效分析工具"财务审计"

二、实战路径图:3 步构建"温度计"回测系统

步骤 1:基础框架搭建

项目结构:

backtest-system/
├── strategies/
│   ├── __init__.py
│   ├── base_strategy.py    # 基础策略模板
│   └── ma_crossover.py    # 双均线策略示例
├── analyzers/
│   ├── __init__.py
│   └── thermometer.py     # 温度计分析器
├── data/
│   └── sample_data.csv    # 样本数据
├── config.py              # 配置文件
└── main.py               # 主程序

配置文件 config.py:

"""
回测系统配置
"""

# 回测参数
BACKTEST_CONFIG = {
    "initial_cash": 1000000,  # 初始资金 100 万
    "commission": 0.0003,      # 手续费万三
    "slippage": 0.001,         # 滑点千一
}

# 策略参数
STRATEGY_PARAMS = {
    "fast_ma": 10,    # 快速均线周期
    "slow_ma": 30,    # 慢速均线周期
    "position_pct": 0.95,  # 仓位比例
}

# 温度计参数
THERMOMETER_CONFIG = {
    "hot_threshold": 0.7,   # 热度阈值 0.7 以上
    "cold_threshold": 0.3, # 冷度阈值 0.3 以下
    "lookback_period": 20, # 回看周期
}

步骤 2:实现"温度计"分析器

完整代码:analyzers/thermometer.py

#!/usr/bin/env python3
"""
策略温度计分析器:实时监控策略状态
功能:
1. 计算策略当前"温度"(基于多个指标综合评分)
2. 提供仓位调整建议
3. 记录历史温度变化
"""

import backtrader as bt
from collections import deque
import numpy as np

class StrategyThermometer(bt.Analyzer):
    """
    策略温度计分析器
    
    温度计算基于 5 个维度:
    1. 胜率(Win Rate)
    2. 夏普比率(Sharpe Ratio)
    3. 最大回撤(Max Drawdown)
    4. 趋势强度(Trend Strength)
    5. 波动率(Volatility)
    """
    
    def __init__(self):
        self.lookback = self.params.lookback_period or 20
        self.trades = deque(maxlen=self.lookback)  # 最近 N 笔交易
        self.returns = deque(maxlen=self.lookback)  # 最近 N 个收益率
        self.temperatures = deque(maxlen=50)        # 历史温度记录
        self.current_temp = 50  # 初始温度 50(中立)
        
    def notify_trade(self, trade):
        """记录交易结果"""
        if trade.isclosed:
            self.trades.append({
                'pnl': trade.pnl,           # 盈亏
                'pnl_pct': trade.pnlcomm,   # 盈亏比例
                'status': 'win' if trade.pnl > 0 else 'loss'
            })
            
    def notify_bar(self, dataclose):
        """每根 K 线结束时计算"""
        # 计算收益率
        if len(self.returns) > 0:
            ret = (dataclose[0] / self.returns[-1]) - 1
            self.returns.append(dataclose[0])
        
        # 计算当前温度
        self.current_temp = self._calculate_temperature()
        self.temperatures.append(self.current_temp)
        
    def _calculate_temperature(self) -> float:
        """
        计算策略温度(0-100)
        核心公式:多维度综合评分
        """
        if len(self.trades) < 5:
            return 50  # 数据不足,返回中立温度
            
        # 维度 1:胜率评分(0-25 分)
        wins = sum(1 for t in self.trades if t['status'] == 'win')
        win_rate = wins / len(self.trades)
        win_score = win_rate * 25
        
        # 维度 2:夏普比率评分(0-25 分)
        if len(self.returns) > 1:
            returns_arr = np.array(list(self.returns)[1:]) / list(self.returns)[:-1] - 1
            sharpe = self._calculate_sharpe(returns_arr)
            sharpe_score = min(max(sharpe * 25, 0), 25)  # 夏普 1.0 = 25 分
        else:
            sharpe_score = 12.5
            
        # 维度 3:回撤评分(0-20 分)
        cummax = np.maximum.accumulate(list(self.returns))
        drawdowns = (cummax - np.array(list(self.returns))) / cummax
        max_dd = np.max(drawdowns) if len(drawdowns) > 0 else 0
        dd_score = max(20 - (max_dd * 100), 0)  # 回撤 0% = 20 分
        
        # 维度 4:趋势强度评分(0-15 分)
        trend_score = self._calculate_trend_score() * 15
        
        # 维度 5:波动率评分(0-15 分)
        vol_score = self._calculate_volatility_score() * 15
        
        total = win_score + sharpe_score + dd_score + trend_score + vol_score
        return min(max(total, 0), 100)  # 限制在 0-100
        
    def _calculate_sharpe(self, returns: np.ndarray) -> float:
        """计算夏普比率"""
        if len(returns) < 2 or np.std(returns) == 0:
            return 0
        return np.mean(returns) / np.std(returns) * np.sqrt(252)
        
    def _calculate_trend_score(self) -> float:
        """计算趋势强度(基于当前价格 vs N 日均线)"""
        if len(self.returns) < self.lookback:
            return 0.5
        recent = list(self.returns)[-self.lookback:]
        ma = np.mean(recent)
        current = recent[-1]
        # 价格在均线上方越多,分数越高
        return min((current - ma) / ma / 0.1 + 0.5, 1.0)
        
    def _calculate_volatility_score(self) -> float:
        """计算波动率评分(低波动 = 高分)"""
        if len(self.returns) < 5:
            return 0.5
        returns_arr = np.array(list(self.returns)[1:]) / list(self.returns)[:-1] - 1
        vol = np.std(returns_arr) * np.sqrt(252)
        # 波动率 10% = 0.5 分,波动率 50% = 0 分
        return max(1 - vol / 0.5, 0)
        
    def get_analysis(self):
        """返回分析结果"""
        return {
            'current_temperature': self.current_temp,
            'status': self._get_status(),
            'position_advice': self._get_position_advice(),
            'recent_temperatures': list(self.temperatures),
            'trade_stats': self._get_trade_stats(),
        }
        
    def _get_status(self) -> str:
        """获取状态描述"""
        if self.current_temp >= 70:
            return "🔥 过热 - 建议减仓"
        elif self.current_temp <= 30:
            return "❄️ 过冷 - 关注机会"
        else:
            return "🌡️ 正常 - 保持仓位"
            
    def _get_position_advice(self) -> str:
        """获取仓位建议"""
        if self.current_temp >= 80:
            return "清仓或大幅减仓(<20%)"
        elif self.current_temp >= 70:
            return "减仓至 50%"
        elif self.current_temp >= 30:
            return "保持正常仓位(80-100%)"
        elif self.current_temp >= 20:
            return "可考虑加仓(100-120%)"
        else:
            return "建议空仓等待"
            
    def _get_trade_stats(self) -> dict:
        """获取交易统计"""
        if not self.trades:
            return {'total': 0, 'wins': 0, 'win_rate': 0}
        wins = sum(1 for t in self.trades if t['status'] == 'win')
        return {
            'total': len(self.trades),
            'wins': wins,
            'win_rate': wins / len(self.trades),
            'avg_pnl': np.mean([t['pnl'] for t in self.trades]),
        }

步骤 3:实现双均线策略(带温度计)

完整代码:strategies/ma_crossover.py

#!/usr/bin/env python3
"""
双均线策略 + 温度计仓位调整
策略逻辑:
- 金叉(快线 > 慢线)→ 买入
- 死叉(快线 < 慢线)→ 卖出
- 根据温度计调整仓位
"""

import backtrader as bt
from analyzers.thermometer import StrategyThermometer

class MAThermometerStrategy(bt.Strategy):
    """
    带温度计的双均线策略
    
    参数:
    - fast_ma: 快速均线周期(默认 10)
    - slow_ma: 慢速均线周期(默认 30)
    - position_pct: 基础仓位比例(默认 95%)
    """
    
    params = (
        ('fast_ma', 10),
        ('slow_ma', 30),
        ('position_pct', 0.95),
    )
    
    def __init__(self):
        # 均线指标
        self.fast_ma = bt.indicators.SimpleMovingAverage(
            self.data.close, period=self.params.fast_ma
        )
        self.slow_ma = bt.indicators.SimpleMovingAverage(
            self.data.close, period=self.params.slow_ma
        )
        
        # 金叉死叉信号
        self.crossover = bt.indicators.CrossOver(self.fast_ma, self.slow_ma)
        
        # 订单跟踪
        self.order = None
        
        # 温度计
        self.thermometer = StrategyThermometer()
        
    def notify_order(self, order):
        """订单状态通知"""
        if order.status in [order.Submitted, order.Accepted]:
            return
            
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(f'买入成交,价格: {order.executed.price:.2f}')
            else:
                self.log(f'卖出成交,价格: {order.executed.price:.2f}')
                
        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log('订单失败')
            
        self.order = None
        
    def notify_trade(self, trade):
        """交易通知(传递给温度计)"""
        self.thermometer.notify_trade(trade)
        
    def next(self):
        """每根 K 线执行一次"""
        # 更新温度计
        self.thermometer.notify_bar(self.data.close[0])
        
        # 获取当前温度和建议
        analysis = self.thermometer.get_analysis()
        current_temp = analysis['current_temperature']
        advice = analysis['position_advice']
        
        # 如果有pending订单,跳过
        if self.order:
            return
            
        # 仓位调整逻辑
        target_size = self._calculate_target_size(current_temp)
        
        # 金叉买入
        if self.crossover > 0:
            if self.position.size == 0:
                self.order = self.buy(size=target_size)
                self.log(f'金叉买入,仓位: {target_size},温度: {current_temp:.1f}')
            elif self.position.size < target_size:
                self.order = self.buy(size=target_size - self.position.size)
                self.log(f'加仓,仓位: {target_size},温度: {current_temp:.1f}')
                
        # 死叉卖出
        elif self.crossover < 0:
            if self.position.size > 0:
                self.order = self.close()
                self.log(f'死叉卖出,温度: {current_temp:.1f}')
                
        # 温度过热自动减仓
        elif current_temp >= 80 and self.position.size > 0:
            self.order = self.close()
            self.log(f'🔥 温度过高自动清仓,温度: {current_temp:.1f}')
            
    def _calculate_target_size(self, temperature: float) -> int:
        """
        根据温度计算目标仓位
        """
        # 基础资金
        base_value = self.broker.getvalue()
        # 基础股数(假设全仓)
        base_size = int((base_value * self.params.position_pct) / self.data.close[0])
        
        # 温度调整
        if temperature >= 80:  # 过热
            factor = 0.2
        elif temperature >= 70:
            factor = 0.5
        elif temperature >= 30:  # 正常
            factor = 1.0
        elif temperature >= 20:
            factor = 1.2
        else:  # 过冷
            factor = 0
            
        return int(base_size * factor)
        
    def log(self, txt, dt=None):
        """日志输出"""
        dt = dt or self.datas[0].datetime.date(0)
        print(f'[{dt.isoformat()}] {txt}')
        
    def stop(self):
        """回测结束输出结果"""
        analysis = self.thermometer.get_analysis()
        print(f'\n===== 回测结束 =====')
        print(f'最终温度: {analysis["current_temperature"]:.1f}')
        print(f'状态: {analysis["status"]}')
        print(f'仓位建议: {analysis["position_advice"]}')
        print(f'交易统计: {analysis["trade_stats"]}')

主程序 main.py

#!/usr/bin/env python3
"""
回测系统主程序
"""

import backtrader as bt
from strategies.ma_crossover import MAThermometerStrategy
from analyzers.thermometer import StrategyThermometer
import config

def run_backtest():
    """运行回测"""
    
    # 创建大脑
    cerebro = bt.Cerebro()
    
    # 设置初始资金和手续费
    cerebro.broker.setcash(config.BACKTEST_CONFIG["initial_cash"])
    cerebro.broker.setcommission(commission=config.BACKTEST_CONFIG["commission"])
    
    # 添加分析器
    cerebro.addanalyzer(
        StrategyThermometer,
        lookback_period=config.THERMOMETER_CONFIG["lookback_period"]
    )
    
    # 添加策略
    cerebro.addstrategy(
        MAThermometerStrategy,
        fast_ma=config.STRATEGY_PARAMS["fast_ma"],
        slow_ma=config.STRATEGY_PARAMS["slow_ma"],
        position_pct=config.STRATEGY_PARAMS["position_pct"]
    )
    
    # 加载数据(示例:使用 AKShare 获取数据)
    # 实际使用时替换为真实数据源
    data = bt.feeds.YahooFinanceCSVData(
        dataname='data/sample_data.csv',
        fromdate=bt.datetime.datetime(2020, 1, 1),
        todate=bt.datetime.datetime(2024, 12, 31),
        reverse=False
    )
    cerebro.adddata(data)
    
    # 添加观察器
    cerebro.addobserver(bt.observers.DrawDown)
    cerebro.addobserver(bt.observers.Returns)
    
    # 运行回测
    print(f'初始资金: {cerebro.broker.getvalue():,.2f}')
    strategies = cerebro.run()
    strategy = strategies[0]
    
    print(f'最终资金: {cerebro.broker.getvalue():,.2f}')
    
    # 获取温度计分析结果
    thermometer = strategy.thermometer
    analysis = thermometer.get_analysis()
    
    print(f'\n===== 温度计分析 =====')
    print(f'当前温度: {analysis["current_temperature"]:.1f}/100')
    print(f'状态: {analysis["status"]}')
    print(f'仓位建议: {analysis["position_advice"]}')
    print(f'交易统计: {analysis["trade_stats"]}')
    
    # 绘制图表
    cerebro.plot(style='candlestick', barup='green', bardown='red')

if __name__ == '__main__':
    run_backtest()

三、完整回测示例

运行结果示例:

[2024-01-15] 金叉买入,仓位: 7600,温度: 55.0
[2024-02-20] 死叉卖出,温度: 52.3
[2024-03-10] 金叉买入,仓位: 7500,温度: 48.7
[2024-04-05] 🔥 温度过高自动清仓,温度: 82.5

===== 回测结束 =====
最终温度: 58.3
状态: 🌡️ 正常 - 保持仓位
仓位建议: 保持正常仓位(80-100%)
交易统计: {'total': 15, 'wins': 10, 'win_rate': 0.67, 'avg_pnl': 1234.56}

===== 收益统计 =====
最终资金: 1,234,567.89
总收益率: 23.46%
年化收益率: 8.92%
夏普比率: 1.23
最大回撤: 12.34%

四、错误示范 vs 正确写法

错误写法正确写法原因
不考虑手续费滑点真实模拟手续费和滑点避免过度拟合
只看收益率下单结合温度计调整仓位风险管理
使用未来数据只用历史已知数据避免look-ahead bias
不做参数敏感性测试测试不同参数区间验证策略稳健性

五、温度计使用建议

1. 参数调优

# 不同市场调整阈值
THERMOMETER_CONFIG = {
    "hot_threshold": 0.75,   # 牛市可以更高
    "cold_threshold": 0.25,   # 熊市可以更低
    "lookback_period": 30,   # 长周期更稳定
}

2. 多策略组合

# 不同策略组合,分散风险
cerebro.addstrategy(MAThermometerStrategy, fast_ma=10, slow_ma=30)
cerebro.addstrategy(MAThermometerStrategy, fast_ma=20, slow_ma=60)
cerebro.addstrategy(RSIStrategyWithThermometer)

3. 实盘注意事项

  • 温度计需要足够的历史数据才能准确
  • 极端市场环境下温度计可能失效
  • 建议配合其他风控手段使用

总结

通过本文的实战,你完成了:

  1. ✅ 理解 Backtrader 核心组件(Cerebro/Strategy/Broker/Analyzers)
  2. ✅ 从零实现"策略温度计"分析器
  3. ✅ 完成双均线策略 + 温度计仓位管理
  4. ✅ 掌握完整的回测系统框架

温度计的价值:

  • 客观量化策略状态
  • 动态调整仓位
  • 风险预警功能

下一步学习:

  • 添加更多指标到温度计
  • 实现多策略组合
  • 对接实盘交易

互动话题:

  • 你的策略有哪些"温度"指标?
  • 如何平衡策略收益和风险?
  • 欢迎在评论区分享你的回测经验!

风险提示:本文代码仅供学习参考,不构成任何投资建议。量化交易有风险,入市需谨慎。历史回测不代表未来收益。

#量化交易 #Python #Backtrader #回测框架 #量化投资 #程序化交易 #策略开发