声明:本文所有代码仅供学习参考,不构成任何投资建议。量化交易有风险,入市需谨慎。
引言:为什么你的回测系统总是"差一点"?
很多量化新手都有过类似经历:
- 回测结果挺好,一实盘就亏钱
- 不知道策略什么时候有效,什么时候失效
- 就像有一把"温度计",能随时显示策略的"体温"就好了
"策略温度计"的核心思路:
- 温度高(策略过热)→ 减少仓位或暂停
- 温度适中(策略正常)→ 保持正常仓位
- 温度低(策略遇冷)→ 可能存在机会
本文带你用 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. 实盘注意事项
- 温度计需要足够的历史数据才能准确
- 极端市场环境下温度计可能失效
- 建议配合其他风控手段使用
总结
通过本文的实战,你完成了:
- ✅ 理解 Backtrader 核心组件(Cerebro/Strategy/Broker/Analyzers)
- ✅ 从零实现"策略温度计"分析器
- ✅ 完成双均线策略 + 温度计仓位管理
- ✅ 掌握完整的回测系统框架
温度计的价值:
- 客观量化策略状态
- 动态调整仓位
- 风险预警功能
下一步学习:
- 添加更多指标到温度计
- 实现多策略组合
- 对接实盘交易
互动话题:
- 你的策略有哪些"温度"指标?
- 如何平衡策略收益和风险?
- 欢迎在评论区分享你的回测经验!
风险提示:本文代码仅供学习参考,不构成任何投资建议。量化交易有风险,入市需谨慎。历史回测不代表未来收益。
#量化交易 #Python #Backtrader #回测框架 #量化投资 #程序化交易 #策略开发