15天搭建ETF量化交易系统Day3—上手经典回测框架


d56e1ce05e839ff980367752bb5803aa.png

搭建过程

3df69d27b7d148c2fbc584a4ee90cd32.png

每个交易者都应该形成一套自己的交易系统。

 
 

很多交易者也清楚知道,搭建自己交易系统的重要性。现实中,从0到1往往是最难跨越的一步。

授人鱼不如授人以渔,为了帮助大家跨出搭建量化系统的第一步,我们决定推出这个主题系列。

这个系列中,我们用Python从0开始一步步搭建出一套ETF量化交易系统(选择ETF标的是因为对于普通交易者来说,ETF相对于选强势股难度要小,而且没有退市风险)。大家可以跟随着我们的实现路径来一起学习,从过程中掌握方法。

掌握了方法之后,你可以换成期货系统、比特币系统、美股系统,然后在实战中不断去完善自己的系统了。

搭建一套ETF量化交易系统涉及多个模块和组件的协同工作,包括数据源模块、量化策略模块、可视化模块、数据库模块、回测评估模块、自动交易模块等等。

DAY1链接如下:15天搭建ETF量化交易系统Day1—数据源模块

DAY2链接如下:15天搭建ETF量化交易系统Day2—图形显示模块

DAY3我们开始涉及回测评估模块,先快速上手市面上非常经典的BackTrader,为下一步策略开发提供基础。

08da7bc493628ebc3a541843c42497f8.png

准备工作

55548b0510769295cc55c5a64ce111df.png

量化交易系统中的回测模块是一个核心组成部分,用于在历史数据上测试和评估交易策略的性能。

回测模块通过模拟历史市场环境,让交易策略在这些历史数据上进行“实战演练”,从而得出策略在过去一段时间内的表现情况,为实际交易提供有力的决策支持。

作为新手,我们决定先使用现成的回测框架,因为现成的框架通常具有完善的文档和示例代码,我们可以快速了解和学习如何使用它们。如果后期发现现成的框架不能满足我们特有需求时候,我们可以选择编写自己的回测框架。

当然,现在学习现成回测框架所投入的时间并不会打水漂,对于我们搭建自己的回测框架也是有帮助的。

我们选择Backtrader作为我们的回测框架。backtrader属于功能相对完善的本地版Python量化回测框架。

首先,在Python环境中安装Backtrader库:

pip install backtrader

然后,在Python脚本中导入Backtrader库:

import backtrader as bt

接下来,我们用双均线策略来熟悉下BackTrader的使用。

badddc4c24349d81901686d12ad2cacd.png

单个股策略回测

1dfba202de2083eae2b73ee7374dcd67.png

定义数据加载函数:使用ak.fund_etf_hist_min_em函数从AKShare获取ETF的分钟级别历史数据,并将其转换为BackTrader可以使用的格式。

def GetStockDatApi(code, start='20240425', end='20240426'):


    df = ak.fund_etf_hist_min_em(symbol=code, start_date=start, end_date=end)


    # 将交易日期设置为索引值
    df.index = pd.to_datetime(df["时间"])
    df.sort_index(inplace=True)
    df.drop(axis=1, columns='时间', inplace=True)


    recon_data = {'High': df.最高, 'Low': df.最低, 'Open': df.开盘, 'Close': df.收盘, \
                  'Volume': df.成交量}
    df_recon = pd.DataFrame(recon_data)


    return df_recon

backtrader的数据加载非常灵活,提供了多种形式的数据接口,可以是CSV文件格式的数据,也可以是DataFrame格式数据。此处我们使用DataFrame格式数据,如下所示:

High   Low  Open  Close  Volume  OpenInterest
时间                                                                
2024-04-25 09:35:00  3.51  3.50  3.50   3.50  321378     0
2024-04-25 09:40:00  3.50  3.50  3.50   3.50  268571     0
2024-04-25 09:45:00  3.50  3.50  3.50   3.50  187412     0
2024-04-25 09:50:00  3.50  3.50  3.50   3.50  208152     0
2024-04-25 09:55:00  3.50  3.50  3.50   3.50  153200     0

定义双均线策略:构建策略的类是继承backtrader.Strategy,然后根据自己的需要重写其中的方法。比如__init__、log、notify_order、notify_trade、next等等。

我们创建一个继承自bt.Strategy的类,在里面定义双均线策略。

关于策略中的指标,backtrader内置了很多类型,直接调用即可。比如移动平均线:

self.sma = bt.indicators.SimpleMovingAverage(


              self.datas[0], period=self.params.maperiod)

next方法中,我们实现一个简单的双均线策略作为交易的逻辑。比如买入条件是MA5上穿MA10(5日收盘价移动平均线和10日收盘价移动平均线形成均线金叉);卖出条件是MA10下穿MA5(5日收盘价移动平均线和10日收盘价移动平均线形成均线死叉)。

class dua_ma_strategy(bt.Strategy):
    # 全局设定交易策略的参数
    params=(
            ('ma_short',30),
            ('ma_long', 60),
           )
    def __init__(self):


        # 指定价格序列
        self.dataclose=self.datas[0].close
        # 初始化交易指令、买卖价格和手续费
        self.order = None
        self.buyprice = None
        self.buycomm = None


        # 添加移动均线指标
        # 5日移动平均线
        self.sma5 = bt.indicators.SimpleMovingAverage(
            self.datas[0], period=self.params.ma_short)
        # 10日移动平均线
        self.sma10 = bt.indicators.SimpleMovingAverage(
            self.datas[0], period=self.params.ma_long)


        self.rsi5 = bt.indicators.RSI_EMA(self.datas[0], period=self.params.ma_short,subplot=True)
        self.william_ad = bt.indicators.WilliamsAD(self.datas[0], subplot=True)


    def log(self, txt, dt=None, doprint=False):
        # 日志函数,用于统一输出日志格式
        if doprint:
            dt = dt or self.datas[0].datetime.date(0)
            print('%s, %s' % (dt.isoformat(), txt))


    def notify_order(self, order):
        """
        订单状态处理
        Arguments:
            order {object} -- 订单状态
        """
        if order.status in [order.Submitted, order.Accepted]:
            # 如订单已被处理,则不用做任何事情
            return
        # 检查订单是否完成
        if order.status in [order.Completed]:
            if order.isbuy():
                self.buyprice = order.executed.price
                self.buycomm = order.executed.comm
                self.log(f'买入:\n价格:{order.executed.price},\
                                成本:{order.executed.value},\
                                手续费:{order.executed.comm}', doprint=True)
            else:
                self.log(f'卖出:\n价格:{order.executed.price},\
                                成本: {order.executed.value},\
                                手续费{order.executed.comm}', doprint=True)
            self.bar_executed = len(self)
        # 订单因为缺少资金之类的原因被拒绝执行
        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log('Order Canceled/Margin/Rejected')
        # 订单状态处理完成,设为空
        self.order = None


    # 记录交易收益情况
    def notify_trade(self, trade):
        """
        交易成果
        Arguments:
            trade {object} -- 交易状态
        """
        if not trade.isclosed:
            return
        # 显示交易的毛利率和净利润
        self.log('Operation Profit, Gross%.2f, Net %.2f' %
                 (trade.pnl, trade.pnlcomm), doprint=True)


    def next(self):
        # 记录收盘价
        self.log('Close, %.2f' % self.dataclose[0])
        # Access -1, because drawdown[0] will be calculated after "next"
        #self.log('DrawDown: %.2f' % self.stats.drawdown.drawdown[-1], doprint=True)
        #self.log('MaxDrawDown: %.2f' % self.stats.drawdown.maxdrawdown[-1], doprint=True)


        if self.order: # 是否有指令等待执行
            return
        # 是否持仓
        if not self.position: # 没有持仓
            # 执行买入条件判断:MA5上扬突破MA10,买入
            if self.sma5[0] > self.sma10[0]:
                self.order = self.buy() # 执行买入
                self.log('Buy Create, %.2f' % self.dataclose[0], doprint = True)
        else:
            # 执行卖出条件判断:MA5下穿跌破MA10,卖出
            if self.sma5[0] < self.sma10[0]:
                self.order = self.sell() # 执行卖出
                self.log('Sell Create, %.2f' % self.dataclose[0], doprint = True)


    # 回测结束后输出
    def stop(self):
        self.log(u'金叉死叉策略 %2d Ending Value %.2f' %
                 (self.params.ma_long, self.broker.getvalue()), doprint=True)

log方法用于打印日志,包含一个使能标志位,可以选择是否打印出日期时间以及txt变量传递值。

notify_order方法用于跟踪交易指令(order)的状态。order具有提交,接受,买入/卖出执行和价格,已取消/拒绝等状态。

notify_trade方法用于跟踪交易的状态,任何已平仓的交易都将报告毛利和净利润。

设置Cerebro引擎:关于策略回测,把数据和策略添加到Cerebro中之外,还有设置一些参数。比如broker的设置,像初始资金、交易佣金,也可以用addsizer设定每次交易买入的股数。

此处,我们创建一个Cerebro对象,并加载ETF数据、添加策略和设置佣金、滑点等参数。

cerebro = bt.Cerebro() # 初始化cerebro回测系统设置 默认 stdstats=True
cerebro.adddata(data) # 将数据传入回测系统
cerebro.addstrategy(dua_ma_strategy) # 将交易策略加载到回测系统中
cerebro.broker.setcash(10000) # 设置初始资本为10,000
cerebro.addsizer(bt.sizers.FixedSize, stake=500) # 设定每次交易买入的股数
cerebro.broker.setcommission(commission=0.002) # 设置交易手续费为 0.2%
cerebro.addobserver(bt.observers.DrawDown)
print('初始资金: %.2f' % cerebro.broker.getvalue())

运行回测:调用Cerebro对象的run()方法来执行回测。

results = cerebro.run() #运行回测系统

回测结束后返回得到执行交易策略时积累的总资金。

初始资金: 10000.00
2024-04-26, Buy Create,
3.54
2024-04-26, 买入:
价格:3.545,                                    成本:1772.5,                                    手续费:3.545
2024-04-30, Sell Create,
3.62
2024-04-30, 卖出:
价格:3.617,                                    成本: 1772.5,                                    手续费3.617
2024-04-30, Operation
Profit, Gross36.00, Net 28.84
......
2024-05-10, Buy Create,
3.66
2024-05-10, 买入:
价格:3.658,                                    成本:1829.0,                                    手续费:3.658
2024-05-10, 金叉死叉策略 60 Ending Value
9980.52
最终资金: 9980.52

此处我们回测的是沪深300ETF(510300)2024年4月25日到2024年5月10日期间的策略执行效果,最终资金从10000变成了9980.52。

由于backtrader内置了Matplotlib,因此我们也可以可视化回测的效果,如下所示:

edcfaca0a43c1971d1e6510e4130c3b3.png

Backtrader可视化回测结果后会产生包含了三个子图的图表上,如图所示:由上往下分别为资金变动图、交易盈亏图、买卖区间图。

2c688674168cbcf85603e4f020ebf5c7.png

批量股策略回测

9ca8cd176aa181ffdbc84e7adc427540.png

当要对A股市场全部ETF执行批量回测时,只需把单只ETF的回测封装好,循环调用即可。比如我们要回测"159998", "159997",  "159996", "159995",  "159994"这几只ETF,那就循环调用单只ETF回测的全部步骤即可。

code_names = ["159998", "159997", "159996", "159995", "159994"]
# 构建一个空的字典用来装数据
result = {}
for name, code in code_names:
    feed_pandas = GetStockDatApi(code, '20240101', '20240510')
    feed_pandas['OpenInterest'] = 0
    # 加载回测期间的数据
    data = bt.feeds.PandasData(dataname=feed_pandas, fromdate=datetime.datetime(2024, 1, 1, 9, 35),
                               todate=datetime.datetime(2024, 5, 10, 15, 0))

回测的结果转换成收益率,用字典存储。有了这个数据集合之后,我们就可以去统计这个策略下有多少ETF上涨、多少ETF下跌、多少ETF涨幅大于10%、多少ETF跌幅大于10%等等。如下所示:

159998 回测
初始资金: 10000.00
最终资金: 9900.3
159997 回测
初始资金: 10000.00
最终资金: 9894.39
159996 回测
初始资金: 10000.00
最终资金: 9945.54
159995 回测
初始资金: 10000.00
最终资金: 9907.6
159994 回测
初始资金: 10000.00
最终资金: 9893.24
{'159998': -0.01,
'159997': -0.01, '159996': -0.01, '159995': -0.01, '159994': -0.01}
最高收益的股票:159998, 达到 -0.01
正收益数量: 0, 负收益数量:5
+10%数量: 0, -10%数量:0
收益10%以上的股票: {}

以上就是量化系统中经典的回测框架Backtrader的入门使用方法,后续随着搭建量化系统设计策略的深入,我们会频繁地调用到这部分内容,大家一定要熟悉。

说明

此系列为连载专栏,完整代码会上传知识星球《玩转股票量化交易》!作为会员们的学习资料。

想要加入知识星球《玩转股票量化交易》的小伙伴记得先微信call我获取福利!

非星球会员需要的话,需要单独联系我购买!

知识星球介绍点击:知识星球《玩转股票量化交易》精华内容概览

ff21052420a5339fdd8e9b64f7318001.jpeg

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值