如何用Backtrader回测自定义AI反转策略?无买卖信号问题排查
问题诊断与修复方案
核心问题分析
你的策略未生成买卖信号和组合价值变化,主要源于以下关键错误:
1. 实时时间判断与回测逻辑冲突
回测模式下,datetime.now() 获取的是当前系统时间,而非数据中的历史时间,导致:
- 周末判断逻辑错误,跳过大量历史数据
- 分钟间隔判断完全不匹配历史数据时间戳,模型训练流程从未启动,
self.model始终为None,AI 预测逻辑无法触发
2. 缺失数据获取方法
策略中调用 self.get_data() 但未定义该方法,无法获取训练数据,模型训练流程中断
3. 数据处理语法错误
prepare_data 和 prepare_features 方法中,data.datetime.get(size(len(self.macd))) 应为 data.datetime.get(size=len(self.macd)),直接抛出语法错误中断执行
4. 目标变量前瞻偏差
TargetIndicator 中用当前收盘价对比前一根收盘价(self.data.close[0] > self.data.close[-1]),回测中 self.data.close[-1] 属于未来数据,导致前瞻偏差,且与模型训练时的目标变量定义不一致
修复后的完整代码
import backtrader as bt import pandas as pd import numpy as np import warnings from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split, GridSearchCV from sklearn.metrics import accuracy_score warnings.filterwarnings("ignore", category=FutureWarning, module="ta.trend") # 自定义绩效分析器:追踪组合价值与盈亏 class PerformanceAnalyzer(bt.Analyzer): def __init__(self): self.portfolio_values = [] self.pnl = [] def notify_trade(self, trade): if trade.isclosed: self.pnl.append(trade.pnl) def next(self): self.portfolio_values.append(self.strategy.broker.get_value()) def get_analysis(self): return { 'cumulative_pnl': sum(self.pnl), 'average_pnl': np.mean(self.pnl) if self.pnl else 0, 'max_pnl': max(self.pnl) if self.pnl else 0, 'min_pnl': min(self.pnl) if self.pnl else 0, 'portfolio_values': self.portfolio_values } # 自定义目标指标:预测下一根K线涨跌(无前瞻偏差) class TargetIndicator(bt.Indicator): lines = ('target',) def next(self): # 用前两根K线对比,避免使用未来数据 if len(self.data) >= 2: self.lines.target[0] = 1 if self.data.close[-1] > self.data.close[-2] else 0 else: self.lines.target[0] = 0 # AI反转策略 class AIReversalStrategy(bt.Strategy): params = ( ('symbol', 'BTCUSD'), ('interval', 15), # 匹配24小时15分钟K线的策略逻辑 ('train_window', 96), # 24小时对应的15分钟K线数量:24*4=96 ) def __init__(self): self.order = None self.buy_signals = [] self.sell_signals = [] self.model = None # 技术指标初始化 self.rsi = bt.indicators.RSI(self.data.close, period=14) self.sma_10 = bt.indicators.SimpleMovingAverage(self.data.close, period=10) self.sma_30 = bt.indicators.SimpleMovingAverage(self.data.close, period=30) self.stoch_k = bt.indicators.StochasticSlow(self.data) self.macd = bt.indicators.MACD(self.data.close) self.target = TargetIndicator(self.data) def next(self): # 数据量足够时才执行后续逻辑 if len(self.data) < self.params.train_window: return # 每24小时重新训练一次模型 if len(self.data) % self.params.train_window == 0: self.train_model() # 模型训练完成后执行预测与交易 if self.model is not None: features = self.prepare_features() if features is None: return prediction = self.model.predict([features])[0] signal = 'Buy' if prediction == 1 else 'Sell' print(f"AI预测信号: {signal} | 当前价格: {self.data.close[0]}") # 买入信号:无持仓时触发 if signal == 'Buy' and not self.position: self.order = self.buy() self.buy_signals.append((self.data.datetime.datetime(), self.data.close[0])) print(f"买入信号执行,价格: {self.data.close[0]}") # 卖出信号:有持仓时触发 elif signal == 'Sell' and self.position: self.order = self.sell() self.sell_signals.append((self.data.datetime.datetime(), self.data.close[0])) print(f"卖出信号执行,价格: {self.data.close[0]}") def prepare_features(self): # 提取当前最新的特征值 features = pd.DataFrame({ 'rsi': [self.rsi[0]], 'sma_10': [self.sma_10[0]], 'sma_30': [self.sma_30[0]], 'stoch_k': [self.stoch_k[0]], 'macd': [self.macd[0]] }).dropna() return features.values[0] if not features.empty else None def train_model(self): # 获取最近24小时的历史数据窗口 data_window = self.data.get(size=self.params.train_window) df = pd.DataFrame({ 'close': [d.close for d in data_window], 'rsi': [self.rsi[i] for i in range(-self.params.train_window, 0)], 'sma_10': [self.sma_10[i] for i in range(-self.params.train_window, 0)], 'sma_30': [self.sma_30[i] for i in range(-self.params.train_window, 0)], 'stoch_k': [self.stoch_k[i] for i in range(-self.params.train_window, 0)], 'macd': [self.macd[i] for i in range(-self.params.train_window, 0)] }).dropna() # 定义目标变量:下一根K线收盘价上涨为1,下跌为0 df['target'] = np.where(df['close'].shift(-1) > df['close'], 1, 0) df = df.dropna() if len(df) < 20: print("数据量不足,跳过模型训练") return # 特征工程与模型训练 features = df[['rsi', 'sma_10', 'sma_30', 'stoch_k', 'macd']] target = df['target'] X_train, X_test, y_train, y_test = train_test_split(features, target, test_size=0.2, random_state=42) model = self.tune_model(X_train, y_train) y_pred = model.predict(X_test) print(f"模型训练完成,准确率: {accuracy_score(y_test, y_pred)*100:.2f}%") self.model = model def tune_model(self, features, target): param_grid = { 'n_estimators': [50, 100], 'max_depth': [None, 10], 'min_samples_split': [2, 5] } rf = RandomForestClassifier(random_state=42) grid_search = GridSearchCV(estimator=rf, param_grid=param_grid, cv=3, scoring='accuracy', n_jobs=-1) grid_search.fit(features, target) return grid_search.best_estimator_ def notify_order(self, order): if order.status in [order.Completed]: if order.isbuy(): print(f"买入成交: 价格={order.executed.price:.2f}, 数量={order.executed.size}") elif order.issell(): print(f"卖出成交: 价格={order.executed.price:.2f}, 数量={order.executed.size}") self.order = None elif order.status in [order.Canceled, order.Margin, order.Rejected]: print("订单被取消/保证金不足/拒绝") self.order = None def notify_trade(self, trade): if trade.isclosed: print(f"交易平仓: 总盈亏={trade.pnl:.2f}, 净盈亏={trade.pnlcomm:.2f}") # 初始化回测引擎 cerebro = bt.Cerebro() cerebro.addstrategy(AIReversalStrategy) # 加载历史数据(需确保CSV包含timestamp,open,high,low,close,volume列) data = pd.read_csv('historical_data.csv', parse_dates=['timestamp']) data.set_index('timestamp', inplace=True) datafeed = bt.feeds.PandasData(dataname=data) cerebro.adddata(datafeed) # 配置经纪人参数 cerebro.broker.set_cash(1000) cerebro.broker.setcommission(commission=0.001) # 添加分析器 cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe_ratio') cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown') cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trade_analyzer') cerebro.addanalyzer(PerformanceAnalyzer, _name='performance') # 添加观察者:显示买卖信号与组合价值 cerebro.addobserver(bt.observers.BuySell) cerebro.addobserver(bt.observers.Value) # 运行回测 results = cerebro.run() final_value = cerebro.broker.get_value() print(f"最终组合价值: {final_value:.2f}") # 绘制回测结果 cerebro.plot(style='candlestick')
关键修复说明
- 回测时间逻辑修正:移除系统时间判断,改用数据长度控制模型训练频率,每96根15分钟K线(对应24小时)重新训练一次模型,匹配策略逻辑
- 补全数据获取方法:通过
self.data.get()获取历史数据窗口,直接从Backtrader指标中提取特征值 - 修复语法错误:修正
get(size=len(...))的参数格式 - 消除前瞻偏差:调整目标变量计算逻辑,避免使用未来数据,统一模型训练与指标的目标定义
- 优化训练效率:简化网格搜索参数,减少回测运行时间
内容的提问来源于stack exchange,提问作者Alex Hebra
相关产品推荐
相关产品推荐

