我来详细介绍在Backtrader中使用Tushare数据的几种方法,从简单到复杂,包括示例代码。

方法1:使用PandasData(推荐,最灵活)

这是最常用的方法,将Tushare数据转换为Pandas DataFrame,然后用Backtrader的PandasData适配器加载。

基本示例

import backtrader as bt
import tushare as ts
import pandas as pd
from datetime import datetime

# 1. 从Tushare获取数据
def get_tushare_data(ts_code='000001.SZ', start_date='20200101', end_date='20231231'):
    """
    获取Tushare股票数据并转换为Backtrader需要的格式
    """
    # 初始化tushare(需要token)
    ts.set_token('你的tushare_token')  # 替换为你的token
    pro = ts.pro_api()
    
    # 获取日线数据
    df = pro.daily(ts_code=ts_code, 
                   start_date=start_date, 
                   end_date=end_date)
    
    # 数据清洗和格式化
    df = df.sort_values('trade_date')  # 按日期排序
    df['trade_date'] = pd.to_datetime(df['trade_date'])  # 转换为datetime
    df.set_index('trade_date', inplace=True)  # 设置日期为索引
    
    # 重命名列以匹配Backtrader的PandasData
    df.rename(columns={
        'open': 'Open',
        'high': 'High', 
        'low': 'Low',
        'close': 'Close',
        'vol': 'Volume'
    }, inplace=True)
    
    # 只保留需要的列
    df = df[['Open', 'High', 'Low', 'Close', 'Volume']]
    
    return df

# 2. 创建自定义的PandasData类(可选,处理Tushare特有字段)
class TusharePandasData(bt.feeds.PandasData):
    """
    自定义PandasData以处理Tushare数据
    可以添加更多字段如换手率、涨跌幅等
    """
    params = (
        ('datetime', None),  # 使用索引作为datetime
        ('open', 'Open'),
        ('high', 'High'),
        ('low', 'Low'),
        ('close', 'Close'),
        ('volume', 'Volume'),
        ('openinterest', -1),  # 无持仓量字段
        # 可以添加额外字段
        # ('pct_chg', 'pct_chg'),  # 如果需要涨跌幅
    )

# 3. 完整的回测示例
class SmaCrossStrategy(bt.Strategy):
    params = (
        ('fast', 10),
        ('slow', 30),
    )
    
    def __init__(self):
        self.fast_sma = bt.indicators.SMA(self.data.close, period=self.params.fast)
        self.slow_sma = bt.indicators.SMA(self.data.close, period=self.params.slow)
        self.crossover = bt.indicators.CrossOver(self.fast_sma, self.slow_sma)
        
    def next(self):
        if not self.position:
            if self.crossover > 0:  # 金叉
                self.buy()
        elif self.crossover < 0:  # 死叉
            self.close()

def run_backtest():
    # 创建Cerebro引擎
    cerebro = bt.Cerebro()
    
    # 获取数据
    print("正在获取数据...")
    df = get_tushare_data('000001.SZ', '20200101', '20231231')
    
    if df.empty:
        print("数据获取失败,请检查网络或token")
        return
    
    print(f"数据量: {len(df)} 条")
    print(f"日期范围: {df.index[0]}{df.index[-1]}")
    
    # 创建数据源
    data = TusharePandasData(dataname=df)
    
    # 添加到引擎
    cerebro.adddata(data)
    
    # 添加策略
    cerebro.addstrategy(SmaCrossStrategy)
    
    # 设置初始资金
    cerebro.broker.setcash(100000.0)
    
    # 设置佣金
    cerebro.broker.setcommission(commission=0.001)  # 0.1%佣金
    
    # 添加分析器
    cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')
    cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe')
    cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')
    cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trades')
    
    # 运行回测
    print('初始资金: %.2f' % cerebro.broker.getvalue())
    results = cerebro.run()
    print('最终资金: %.2f' % cerebro.broker.getvalue())
    
    # 打印分析结果
    strat = results[0]
    
    print("\n=== 策略绩效 ===")
    print(f"总收益率: {strat.analyzers.returns.get_analysis()['rtot']:.2%}")
    
    sharpe_ratio = strat.analyzers.sharpe.get_analysis()
    print(f"夏普比率: {sharpe_ratio['sharperatio']:.3f}")
    
    drawdown = strat.analyzers.drawdown.get_analysis()
    print(f"最大回撤: {drawdown['max']['drawdown']:.2%}")
    print(f"最长回撤周期: {drawdown['max']['len']} 天")
    
    trades = strat.analyzers.trades.get_analysis()
    if 'total' in trades:
        print(f"总交易次数: {trades['total']['total']}")
        print(f"盈利交易: {trades['won']['total']}")
        print(f"亏损交易: {trades['lost']['total']}")
        if trades['won']['total'] > 0:
            print(f"胜率: {trades['won']['total']/trades['total']['total']:.2%}")
    
    # 绘制图表
    cerebro.plot(style='candlestick', volume=True)

if __name__ == '__main__':
    run_backtest()

方法2:使用通用CSVData(保存为CSV再读取)

如果不想每次都从Tushare API获取,可以先保存到本地CSV。

import backtrader as bt
import tushare as ts
import pandas as pd
import os

class TushareCSVData(bt.feeds.GenericCSVData):
    """
    自定义CSV数据加载器
    """
    params = (
        ('datetime', 0),  # 日期列索引
        ('open', 1),      # 开盘价列索引
        ('high', 2),      # 最高价列索引
        ('low', 3),       # 最低价列索引
        ('close', 4),     # 收盘价列索引
        ('volume', 5),    # 成交量列索引
        ('openinterest', -1),  # 无持仓量
        ('dtformat', '%Y-%m-%d'),  # 日期格式
        ('nullvalue', 0.0),
    )

def download_and_save_tushare_data(ts_code='000001.SZ', 
                                   start_date='20200101', 
                                   end_date='20231231',
                                   save_path='data.csv'):
    """下载Tushare数据并保存为CSV"""
    ts.set_token('你的tushare_token')
    pro = ts.pro_api()
    
    df = pro.daily(ts_code=ts_code, start_date=start_date, end_date=end_date)
    
    # 格式化
    df = df.sort_values('trade_date')
    df['trade_date'] = pd.to_datetime(df['trade_date'])
    
    # 保存为CSV
    df.to_csv(save_path, index=False)
    print(f"数据已保存到: {save_path}")
    return save_path

# 使用示例
csv_file = download_and_save_tushare_data('000001.SZ')
data = TushareCSVData(dataname=csv_file)

方法3:使用自定义Data Feed(最灵活,但复杂)

对于需要特殊处理的场景,可以创建完全自定义的Data Feed。

class TushareDirectData(bt.feeds.DataBase):
    """
    直接连接Tushare的Data Feed(示例)
    注意:这需要处理实时数据连接,实际使用较少
    """
    params = (
        ('ts_code', '000001.SZ'),
        ('start_date', '20200101'),
        ('end_date', '20231231'),
    )
    
    def __init__(self):
        super().__init__()
        # 初始化tushare连接
        ts.set_token('你的tushare_token')
        self.pro = ts.pro_api()
        self.data_df = None
        self.idx = 0
        
    def start(self):
        """开始加载数据"""
        self.data_df = self.pro.daily(
            ts_code=self.p.ts_code,
            start_date=self.p.start_date,
            end_date=self.p.end_date
        )
        self.data_df = self.data_df.sort_values('trade_date')
        self.idx = 0
        
    def _load(self):
        """加载下一行数据"""
        if self.idx >= len(self.data_df):
            return False
            
        row = self.data_df.iloc[self.idx]
        
        # 设置当前bar的数据
        self.lines.datetime[0] = bt.date2num(pd.to_datetime(row['trade_date']))
        self.lines.open[0] = row['open']
        self.lines.high[0] = row['high']
        self.lines.low[0] = row['low']
        self.lines.close[0] = row['close']
        self.lines.volume[0] = row['vol']
        self.lines.openinterest[0] = 0
        
        self.idx += 1
        return True

完整的多股票回测示例

def multi_stock_backtest():
    """多股票回测示例"""
    cerebro = bt.Cerebro()
    
    # 股票列表
    stocks = ['000001.SZ', '000002.SZ', '000858.SZ']
    
    for stock_code in stocks:
        try:
            # 获取数据
            df = get_tushare_data(stock_code, '20200101', '20231231')
            
            if not df.empty:
                # 创建数据源
                data = TusharePandasData(dataname=df, name=stock_code)
                cerebro.adddata(data)
                print(f"已添加: {stock_code}")
            else:
                print(f"跳过 {stock_code},无数据")
        except Exception as e:
            print(f"获取 {stock_code} 数据失败: {e}")
    
    if len(cerebro.datas) == 0:
        print("没有可用的数据")
        return
    
    # 添加策略
    cerebro.addstrategy(SmaCrossStrategy)
    
    # 设置初始资金和佣金
    cerebro.broker.setcash(100000.0)
    cerebro.broker.setcommission(commission=0.001)
    
    # 运行回测
    print(f"初始资金: {cerebro.broker.getvalue():.2f}")
    cerebro.run()
    print(f"最终资金: {cerebro.broker.getvalue():.2f}")
    
    # 绘图
    cerebro.plot()

Tushare数据字段映射表

Tushare字段 Backtrader字段 说明
trade_date datetime 交易日期
open open 开盘价
high high 最高价
low low 最低价
close close 收盘价
vol volume 成交量
amount - 成交额(需特殊处理)
pct_chg - 涨跌幅(可添加为额外字段)

实用技巧和注意事项

1. 数据预处理

def preprocess_tushare_data(df):
    """数据预处理"""
    # 1. 处理缺失值
    df = df.fillna(method='ffill')
    
    # 2. 处理异常值
    df = df[(df['High'] >= df['Low']) & (df['Volume'] > 0)]
    
    # 3. 添加额外指标
    df['Returns'] = df['Close'].pct_change()
    
    # 4. 确保索引是datetime
    df.index = pd.to_datetime(df.index)
    
    return df

2. 使用缓存提高效率

import pickle
import hashlib
from functools import lru_cache

@lru_cache(maxsize=10)
def get_cached_tushare_data(ts_code, start_date, end_date):
    """带缓存的数据获取"""
    cache_key = f"{ts_code}_{start_date}_{end_date}"
    cache_file = f"cache/{hashlib.md5(cache_key.encode()).hexdigest()}.pkl"
    
    if os.path.exists(cache_file):
        with open(cache_file, 'rb') as f:
            return pickle.load(f)
    else:
        df = get_tushare_data(ts_code, start_date, end_date)
        os.makedirs('cache', exist_ok=True)
        with open(cache_file, 'wb') as f:
            pickle.dump(df, f)
        return df

3. 处理复权数据

def get_adj_data(ts_code, start_date, end_date):
    """获取复权数据"""
    ts.set_token('你的token')
    pro = ts.pro_api()
    
    # 获取复权因子
    df_adj = pro.adj_factor(ts_code=ts_code, 
                           trade_date='')
    
    # 获取前复权数据
    df = pro.pro_bar(ts_code=ts_code, 
                    adj='qfq',  # qfq:前复权, hfq:后复权
                    start_date=start_date,
                    end_date=end_date)
    
    # ... 后续处理
    return df

4. 常见问题解决

问题1:数据时间范围不正确

# 确保数据包含回测所需的时间范围
start_date = '20190101'  # 比回测开始时间早,用于计算指标
end_date = '20231231'

问题2:成交量单位不一致

# Tushare的成交量单位是"手",需要转换为"股"
df['Volume'] = df['Volume'] * 100  # 1手 = 100股

问题3:非交易日数据缺失

# 填充非交易日
df = df.asfreq('D')  # 设置为每日频率
df = df.fillna(method='ffill')  # 向前填充

5. 优化建议

  1. 使用Tushare Pro:免费版有频率限制,Pro版更稳定
  2. 批量下载数据:避免在循环中频繁调用API
  3. 本地缓存:减少API调用次数
  4. 错误处理:添加重试机制
  5. 数据验证:检查数据完整性和准确性

总结

使用Tushare数据配合Backtrader的最佳实践

  1. 使用PandasData方法(方法1),灵活且高效
  2. 做好数据预处理缓存
  3. 注意数据字段映射单位转换
  4. 添加适当的错误处理日志记录

这样就能充分利用 Tushare 丰富的数据和 Backtrader 强大的回测功能了。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐