基于Backtrader量化回测框架的MACD指标回测

本文仅展示代码,建议使用jupyter notebook运行此代码,说明文档详见
基于Backtrader量化回测框架的MACD指标回测说明文档

有任何问题,欢迎评论区留言咨询!

导入包

import pandas as pd
import matplotlib.pyplot as plt
import backtrader as bt
import akshare as ak
from datetime import datetime
import PySimpleGUI as sg
from backtrader_plotting import Bokeh
from backtrader_plotting.schemes import Tradimo

plt.rcParams['font.sans-serif'] = ['SimHei']  #显示中文
plt.rcParams['axes.unicode_minus']=False #用来正常显示负号

%matplotlib inline

加载数据

#Step1.数据加载

#获取user输入
stock_code = sg.popup_get_file('示例sh600600')
start_date = sg.popup_get_file('示例20210101')
end_date = sg.popup_get_file('示例20240202')

format_string = "%Y%m%d" #指定日期格式
start_date = datetime.strptime(start_date, format_string).date()            # 将日期转换为datetime对象
end_date = datetime.strptime(end_date, format_string).date()

#获取沪深300数据作为基准
hs300 = ak.index_zh_a_hist(symbol = '000300',period='daily',start_date=start_date,end_date=end_date)
hs300 = hs300.iloc[:, :6]
hs300.columns = ['date','open','close','high','low','volume']
hs300.index = pd.to_datetime(hs300.date)
hs300['openinterest'] = 0
columns_to_keep = ['open','high','low','close','volume','openinterest']    # 转换为backtrader要求的数据格式
hs300 = hs300[columns_to_keep].copy()
print('沪深300行情数据')
print(hs300.head())
print(f'{"-" * 80}')

#获取股票数据
df = ak.stock_zh_a_daily(symbol=stock_code,start_date=start_date,end_date=end_date)
df.index = pd.to_datetime(df.date)
df['openinterest'] = 0
df = df[columns_to_keep].copy()
print(stock_code,'数据')
print(df.head())

策略构建

#Step2.构建策略
class Strategy_MACD(bt.Strategy):
    
    #设置全局策略参数
    params = (
        ("short_period", 12),
        ("long_period", 26),
        ("signal_period", 9),
    )
    
    #日志函数
    def log(self, txt, dt=None):
        dt = dt or self.datas[0].datetime.date(0)
        print('date:%s, %s' % (dt.isoformat(), txt))
    
    def __init__(self):
        
        # 初始化交易指令、买卖价格和手续费
        self.order = None
        self.buyprice = None
        self.buycomm = None
        
        #添加MACD指标
        self.macd = bt.indicators.MACD(
            self.data.close,
            period_me1=self.params.short_period,
            period_me2=self.params.long_period,
            period_signal=self.params.signal_period,
        )
    
    # 处理和打印订单信息    
    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('买入已执行,%.2f' % order.executed.price) # 记录日志
            elif order.issell():
                self.log('卖出已执行,%.2f' % order.executed.price)
                
            self.bar_executed = len(self)
            
        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 trade.pnlcomm > 0:  # 如果净收益大于0,就认为这次交易盈利,否则认为这次交易亏损(同时输出交易编号)
            self.log(f'交易获利: {trade.ref}')
        else:
            self.log(f'交易亏损: {trade.ref}')  
    
    #主要的循环策略执行部分
    def next(self):
        
        # 当前资产总价
        total_value = self.broker.getvalue()
        
        # 检查是否有待处理订单,如果有就不执行此轮操作
        if self.order:
            return
        
        # 回测最后一天停止交易
        if self.datas[0].datetime.date(0) == end_date:
            return 
        
        # 买入卖出策略
        
        #检查当前仓位position
        if not self.position:
            # 如果仓位为0,可以进行BUY买入操作
            if self.macd.macd[0] > self.macd.signal[0]:     # 检查是否满足买入条件
                self.log("总资产价格:%.2f元" % total_value)
                print("{:-^50s}".format("Split Line"))
                self.log('买入创建,%.2f' % self.data.close[0])
                self.order = self.buy()
                
        else:
            # 如果该股票仓位>0 ,可以进行SELL卖出操作
            if self.macd.macd[0] < self.macd.signal[0]:   # 检查是否满足卖出条件
                self.log("总资产价格:%.2f元" % total_value) 
                print("{:-^50s}".format("Split Line"))
                self.log('卖出创建,%.2f' % self.data.close[0])
                self.order = self.sell()

策略设置及执行

#Step3.策略设置及执行

def run_backtest(strategy, data, startcash, start, end):
    # 执行回测
    
    # 实例化Cerebro回测引擎
    cerebro = bt.Cerebro()
    # 添加数据
    datafeed1 = bt.feeds.PandasData(dataname=df,fromdate=start_date,todate=end_date)
    datafeed2 = bt.feeds.PandasData(dataname=hs300,fromdate=start_date,todate=end_date)
    cerebro.adddata(datafeed1, name=stock_code)
    cerebro.adddata(datafeed2,name='sh000300')
    # 添加策略
    cerebro.addstrategy(Strategy_MACD)
    # 设置初始投资总额
    cerebro.broker.setcash(startcash)
    # 设置交易佣金(双边万三)
    cerebro.broker.setcommission(commission=0.0003)
    # 设置滑点(双边万一)
    cerebro.broker = bt.brokers.BackBroker(slip_perc=0.0001)
    # 设置每笔交易的股票数量100
    cerebro.addsizer(bt.sizers.FixedSize, stake=100)
    # 添加策略分析指标
    cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='tradeanalyzer')
    cerebro.addanalyzer(bt.analyzers.AnnualReturn, _name='annualReturn')    # 年度回报
    cerebro.addanalyzer(bt.analyzers.Returns, _name='_Returns', tann=252)  # 年化收益
    cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')          # 回撤
    cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe')        # 夏普率
    cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')          # 收益
    cerebro.addanalyzer(bt.analyzers.TimeReturn,_name='_TimeReturn')
    #cerebro.addanalyzer(bt.analyzers.TimeReturn, data=datafeed2, _name='benchmark_returns')
    # 添加观测器
    cerebro.addobserver(bt.observers.DrawDown)
    cerebro.addobserver(bt.observers.Benchmark, data=datafeed2)
    #cerebro.addobserver(bt.observers.TimeReturn, data=datafeed1)
    
    # 运行回测
    results = cerebro.run()
    
    #计算胜率
    total_trades = results[0].analyzers.tradeanalyzer.get_analysis()['total']['total']
    won_trades = results[0].analyzers.tradeanalyzer.get_analysis()['won']['total']
    win_rate = (won_trades / total_trades) * 100 if total_trades > 0 else 0

    # 打印分析器输出结果
    print(f"初始资金: {startcash}\n回测期间:{start_date.strftime('%Y-%m-%d')}:{end_date.strftime('%Y-%m-%d')}")
    print('年度汇报:', results[0].analyzers.annualReturn.get_analysis())
    print('年化收益%:', results[0].analyzers._Returns.get_analysis()['rnorm100'])
    print('最大回撤比例%:', results[0].analyzers.drawdown.get_analysis().max.drawdown)
    print('夏普比率:', results[0].analyzers.sharpe.get_analysis()['sharperatio'])
    print('胜率%:', win_rate)
    print('累计收益:', results[0].analyzers.returns.get_analysis()['rtot'])
    
    
    
    # 提取收益序列
    pnl = pd.Series(results[0].analyzers._TimeReturn.get_analysis())
    # 计算累计收益
    cumulative = (pnl + 1).cumprod()
    # 计算回撤序列
    max_return = cumulative.cummax()
    drawdown = (cumulative - max_return) / max_return
    # 计算收益评价指标
    import pyfolio as pf
    # 按年统计收益指标
    perf_stats_year = (pnl).groupby(pnl.index.to_period('y')).apply(lambda data: pf.timeseries.perf_stats(data)).unstack()
    # 统计所有时间段的收益指标
    perf_stats_all = pf.timeseries.perf_stats((pnl)).to_frame(name='all')
    perf_stats = pd.concat([perf_stats_year, perf_stats_all.T], axis=0)
    perf_stats_ = round(perf_stats,4).reset_index()
    
    
    # 绘制图形
    import matplotlib.pyplot as plt
    plt.rcParams['axes.unicode_minus'] = False  # 用来正常显示负号
    import matplotlib.ticker as ticker # 导入设置坐标轴的模块
    plt.style.use('seaborn') # plt.style.use('dark_background')
    
    fig, (ax0, ax1) = plt.subplots(2,1, gridspec_kw = {'height_ratios':[1.5, 4]}, figsize=(20,8))
    cols_names = ['date', 'Annual\nreturn', 'Cumulative\nreturns', 'Annual\nvolatility',
    'Sharpe\nratio', 'Calmar\nratio', 'Stability', 'Max\ndrawdown',
    'Omega\nratio', 'Sortino\nratio', 'Skew', 'Kurtosis', 'Tail\nratio',
    'Daily value\nat risk']
    
    # 绘制表格
    ax0.set_axis_off() # 除去坐标轴
    table = ax0.table(cellText = perf_stats_.values, 
    bbox=(0,0,1,1), # 设置表格位置, (x0, y0, width, height)
    rowLoc = 'right', # 行标题居中
    cellLoc='right' ,
    colLabels = cols_names, # 设置列标题
    colLoc = 'right', # 列标题居中
    edges = 'open' # 不显示表格边框
    )
    table.set_fontsize(13)
    
    # 绘制累计收益曲线
    ax2 = ax1.twinx()
    ax1.yaxis.set_ticks_position('right') # 将回撤曲线的 y 轴移至右侧
    ax2.yaxis.set_ticks_position('left') # 将累计收益曲线的 y 轴移至左侧
    # 绘制回撤曲线
    drawdown.plot.area(ax=ax1, label='drawdown (right)', rot=0, alpha=0.3, fontsize=13, grid=False)
    # 绘制累计收益曲线
    (cumulative).plot(ax=ax2, color='#F1C40F' , lw=3.0, label='cumret (left)', rot=0, fontsize=13, grid=False)
    # 不然 x 轴留有空白
    ax2.set_xbound(lower=cumulative.index.min(), upper=cumulative.index.max())
    # 主轴定位器:每 5 个月显示一个日期:根据具体天数来做排版
    ax2.xaxis.set_major_locator(ticker.MultipleLocator(100)) 
    # 同时绘制双轴的图例
    h1,l1 = ax1.get_legend_handles_labels()
    h2,l2 = ax2.get_legend_handles_labels()
    plt.legend(h1+h2,l1+l2, fontsize=12, loc='upper left', ncol=1)
    
    fig.tight_layout() # 规整排版
    plt.show()
    
    
    
    return cerebro, results

交易分析与评价

#Step4.交易分析与评价

def evaluate_results(cerebro, results):
    # 交易分析与评价
    
    #获取回测结束后的总资金
    portvalue = cerebro.broker.getvalue()
    #打印结果
    print(f'最终资金: {round(portvalue,2)}')
    
def plot_results(cerebro):
    # 交易过程可视化
    
    b = Bokeh(style='bar', plot_mode='single',show=True,scheme=Tradimo())
    cerebro.plot(b)
    #cerebro.plot(style='line')

主函数

if __name__ == '__main__':
    startcash = 100000.0
    cerebro, results = run_backtest(Strategy_MACD, df, startcash, start_date, end_date)
    evaluate_results(cerebro, results)
    plot_results(cerebro)

本文仅代表个人观点,所涉及标的不作推荐,也不构成任何投资建议,仅供参考。如有错误,欢迎批评指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Felix.Chan

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值