中国大陆使用 Backtrader 分析股票

在中国大陆使用 Backtrader 分析股票时,你可以通过 Tushare 或 Baostock 获取免费的股票数据。下面是一个完整的 Python 脚本,演示了如何使用 Backtrader 进行股票分析和回测。— tushare and skdhare 都调通了

import backtrader as bt
import pandas as pd
import tushare as ts
import datetime
import akshare as ak
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties

# Set Chinese display, use Microsoft YaHei to avoid missing glyphs
plt.rcParams["font.family"] = ["Microsoft YaHei"]

# 设置 Tushare token(需要在 Tushare 官网注册获取)
ts.set_token('866b8c22a26277af04f65c453d61300c740217a4ee2e57b33c7cd199')
pro = ts.pro_api()

class ChinaStockAnalyzer:
    def __init__(self, stock_code, start_date, end_date, initial_cash=00000):
        """
        初始化股票分析器
        
        参数:
            stock_code: 股票代码,格式为 '000001.SZ' 或 '600000.SH'
            start_date: 开始日期,格式为 '2020-01-01'
            end_date: 结束日期,格式为 '2021-01-01'
            initial_cash: 初始资金
        """
        self.stock_code = stock_code
        self.start_date = start_date
        self.end_date = end_date
        self.initial_cash = initial_cash
        self.cerebro = bt.Cerebro()
        self.cerebro.broker.set_cash(initial_cash)
        # 设置手续费为万三
        self.cerebro.broker.setcommission(commission=0.0003)
        
    def get_data(self, source='tushare'):
        """获取股票数据"""
        if source == 'tushare':
            # 使用 Tushare 获取数据
            df = pro.daily(ts_code=self.stock_code, start_date=self.start_date.replace('-', ''), 
                          end_date=self.end_date.replace('-', ''))
            # 按日期升序排列
            df = df.sort_values('trade_date')
            # 转换日期格式
            df['trade_date'] = pd.to_datetime(df['trade_date'])
            # 重命名列名以符合 Backtrader 的要求
            df = df.rename(columns={
                'trade_date': 'datetime',
                'open': 'open',
                'high': 'high',
                'low': 'low',
                'close': 'close',
                'vol': 'volume',
                'amount':  'amount'
            })
            # 设置日期为索引
            df = df.set_index('datetime')
            # 选择 Backtrader 需要的列
            df = df[['open', 'high', 'low', 'close', 'volume', 'amount']]
            
        elif source == 'akshare':
            # 转换股票代码为akshare格式
            symbol = self.stock_code
            if symbol.endswith('.SH'):
                symbol = 'sh' + symbol[:6]
            elif symbol.endswith('.SZ'):
                symbol = 'sz' + symbol[:6]
            # Use akshare to get data
            stock_zh_a_hist_df = ak.stock_zh_a_hist_tx(
                symbol=symbol,
                start_date=self.start_date,
                end_date=self.end_date,
                adjust=""
            )
            # 按实际英文列名重命名
            df = stock_zh_a_hist_df.rename(columns={
                'date': 'datetime',
                'open': 'open',
                'high': 'high',
                'low': 'low',
                'close': 'close',
                'amount': 'amount'
            })
            df['datetime'] = pd.to_datetime(df['datetime'])
            df = df.set_index('datetime')
            # 若无volume列则用amount填充
            if 'volume' not in df.columns:
                df['volume'] = df['amount']
            # 调整列顺序
            df = df[['open', 'high', 'low', 'close', 'volume', 'amount']]
        
        else:
            raise ValueError("数据源必须是 'tushare' 或 'akshare'")
            
        return df
        
    def add_data(self, source='tushare'):
        """将数据添加到 Backtrader"""
        df = self.get_data(source)
        data = bt.feeds.PandasData(dataname=df)
        self.cerebro.adddata(data)
        
    def add_strategy(self, strategy_class, **kwargs):
        """添加交易策略"""
        self.cerebro.addstrategy(strategy_class, **kwargs)
        
    def add_analyzers(self):
        """添加分析器"""
        self.cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe')
        self.cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')
        self.cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')
        self.cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trade_analyzer')
        
    def run(self):
        """运行回测"""
        self.add_analyzers()
        results = self.cerebro.run()
        self.strategy = results[0]
        return results
        
    def plot(self):
        """绘制回测结果"""
        self.cerebro.plot(style='candlestick', barup='red', bardown='green')
        
    def print_results(self):
        """打印回测结果"""
        print(f"初始资金: {self.initial_cash:.2f}")
        print(f"最终资金: {self.cerebro.broker.getvalue():.2f}")
        print(f"总收益率: {(self.cerebro.broker.getvalue() / self.initial_cash - 1) * 100:.2f}%")
        print(f"夏普比率: {self.strategy.analyzers.sharpe.get_analysis()['sharperatio']:.3f}")
        print(f"年化收益率: {self.strategy.analyzers.returns.get_analysis()['rnorm100']:.2f}%")
        print(f"最大回撤: {self.strategy.analyzers.drawdown.get_analysis()['max']['drawdown']:.2f}%")
        
        # 打印交易分析
        trade_analysis = self.strategy.analyzers.trade_analyzer.get_analysis()
        print("\n交易分析:")
        print(f"总交易次数: {trade_analysis.total.closed}")
        print(f"盈利交易次数: {trade_analysis.won.total}")
        print(f"亏损交易次数: {trade_analysis.lost.total}")
        print(f"胜率: {trade_analysis.won.total / trade_analysis.total.closed * 100:.2f}%")
        print(f"平均盈利: {trade_analysis.won.pnl.average:.2f}")
        print(f"平均亏损: {trade_analysis.lost.pnl.average:.2f}")
        print(f"盈亏比: {-trade_analysis.won.pnl.average / trade_analysis.lost.pnl.average:.2f}")

# 示例策略:双均线策略
class SmaCross(bt.Strategy):
    params = (
        ('fast', 5),  # 短期均线周期
        ('slow', 20),  # 长期均线周期
        ('printlog', False),  # 是否打印交易日志
    )
    
    def __init__(self):
        # 初始化技术指标
        self.dataclose = self.datas[0].close
        self.fast_sma = bt.indicators.SMA(self.dataclose, period=self.params.fast)
        self.slow_sma = bt.indicators.SMA(self.dataclose, 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:
                size = int(self.broker.getcash() / self.dataclose[0] * 0.95)  # 使用95%的资金买入
                self.buy(size=size)
                if self.params.printlog:
                    print(f'买入信号: 价格={self.dataclose[0]:.2f}, 数量={size}')
        # 有持仓
        else:
            # 快线下穿慢线,卖出
            if self.crossover < 0:
                self.close()
                if self.params.printlog:
                    print(f'卖出信号: 价格={self.dataclose[0]:.2f}')
    
    def log(self, txt, dt=None, doprint=False):
        """日志函数"""
        if self.params.printlog or doprint:
            dt = dt or self.datas[0].datetime.date(0)
            print(f'{dt.isoformat()} {txt}')
    
    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}, 成本={order.executed.value:.2f}, 手续费={order.executed.comm:.2f}')
            else:  # 卖出
                self.log(f'卖出执行: 价格={order.executed.price:.2f}, 成本={order.executed.value:.2f}, 手续费={order.executed.comm:.2f}')
                
        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log('订单取消/保证金不足/拒绝')
            
        # 记录无挂单
        self.order = None
    
    def notify_trade(self, trade):
        if not trade.isclosed:
            return
            
        self.log(f'交易利润, 毛利润={trade.pnl:.2f}, 净利润={trade.pnlcomm:.2f}')

if __name__ == "__main__":
    # 初始化分析器
    analyzer = ChinaStockAnalyzer(
        stock_code=f'601288.SH',  # tushare格式为 '601288.SH',akshare格式为 'sh601288','sz000001'为上证指数
        start_date='20230101',
        end_date='20250523',
        initial_cash=250000
    )
    
    # 添加数据
    analyzer.add_data(source='tushare')
    #analyzer.add_data(source='akshare')
    # 添加策略
    analyzer.add_strategy(SmaCross, fast=5, slow=20)
    
    # 运行回测
    results = analyzer.run()
    
    # 打印结果
    analyzer.print_results()
    
    # 绘制结果
    analyzer.plot()

这个脚本提供了一个完整的框架,用于分析中国 A 股市场的股票。主要功能包括:

  1. 数据获取:支持使用 Tushare 或 akshare 获取股票数据
  2. 双均线策略:实现了基于短期和长期移动平均线交叉的交易策略
  3. 绩效分析:计算夏普比率、年化收益率、最大回撤等关键指标
  4. 交易分析:统计交易次数、胜率、盈亏比等
  5. 可视化:绘制股票价格和回测结果图表

使用前请确保安装了所需的库:pip install backtrader tushare akshare matplotlib pandas,并在 Tushare 官网注册获取 token。你可以通过修改参数来分析不同的股票和时间段,也可以扩展策略类来实现自定义的交易策略。

c:/Users/liumt/Documents/backtrader_chinese_stocks.py
初始资金: 100000.00
最终资金: 131331.44
总收益率: 31.33%
夏普比率: 0.583
年化收益率: 12.66%
最大回撤: 23.12%

交易分析:
总交易次数: 17
盈利交易次数: 10
亏损交易次数: 7
胜率: 58.82%
平均盈利: 6853.56
平均亏损: -5570.25
盈亏比: 1.23

这些输出结果是基于双均线策略(5 日和 20 日均线)对贵州茅台(600519.SH)在 2020 年 1 月 1 日至 2023 年 1 月 1 日期间的回测分析。下面是关键指标的详细解释:
核心绩效指标
初始资金与最终资金
初始资金: 100,000.00 元
最终资金: 131,331.44 元
结论: 策略在三年间实现了 31.33% 的总收益,表现优于持有现金。
收益率与风险指标
总收益率: 31.33%
三年累计收益,未考虑时间价值。
年化收益率: 12.66%
将总收益按复利方式换算为每年的平均收益,便于跨周期比较。
夏普比率: 0.583
每承担 1 单位风险可获得的超额回报(相对于无风险利率)。通常认为夏普比率 > 1 为优秀,此处表明策略风险调整后收益一般。
最大回撤: 23.12%
策略在测试期间的最大亏损幅度(从高点到低点的最大跌幅)。这意味着投资者可能经历过 23.12% 的账面亏损,需评估自身风险承受能力。
交易分析指标
交易频率与胜率
总交易次数: 17 笔
三年间平均每年交易约 6 次,属于中低频交易。
胜率: 58.82%(10 胜 / 7 负)
超过半数的交易盈利,但需结合盈亏比综合评估。
盈亏比分析
平均盈利: 6,853.56 元
平均亏损: -5,570.25 元
盈亏比: 1.23
平均每笔盈利是亏损的 1.23 倍,表明策略在 “大赚小赔” 方面表现一般(理想情况应 > 2)。
综合评估
优势:策略实现了正收益,年化 12.66% 优于多数稳健理财,但需承担 23.12% 的最大回撤风险。
改进方向:
优化参数(如调整均线周期)提高夏普比率和盈亏比。
增加止损机制控制单笔亏损幅度。
考虑组合投资,分散单一股票风险。
注意事项
回测陷阱:历史表现不代表未来,实际交易中可能面临滑点、流动性不足等问题。
市场适应性:双均线策略在趋势明显的市场表现较好,但在震荡市可能频繁止损。
风险提示:23.12% 的最大回撤意味着 10 万元本金可能亏损至 7.69 万元,需谨慎使用杠杆。
如需进一步优化策略,可以尝试调整参数、更换股票或引入多因子模型。