利用Backtrader进行期权回测之四:Covered Call策略

在前面的三篇文章中,解决了期权数据获取和实现期权策略的一些技术问题。在这篇文章中,我要实现一个完整的covered call期权策略。Covered call是最简单的期权策略之一,就是持有股票并卖出认购期权。这里我用深交所沪深300ETF(159919)和对应的ETF期权来实现。

策略所需的数据

为了实现covered call策略,我需要三类数据:

  • 沪深300ETF的日线数据
  • 期权的日线数据
  • 期权合约数据

其中前面两类都可以从通达信软件里下载,而期权合约数据可以从深交所网站下载。具体的下载和转换的细节可以参考前面的文章,这里假设已经下载并且转换成dataframe了。

沪深300ETF数据:

图1:沪深300ETF
期权日线数据:
图2:期权日线数据
期权合约数据:
图3:期权合约数据

策略类实现

下面是完整的CoveredCallStrategy代码:

import pandas as pd
import backtrader as bt
from backtrader.feeds import PandasData

# Covered Call 策略
class CoveredCallStrategy(bt.Strategy):
    params = (
        ('opts', None),				# 期权合约信息
        ('etf_size', 10000),		# 每手期权对应的基金份数
    )
    
    def __init__(self):
        self.month = None
        self.num_of_day = 0 
    
    def prenext(self):
        self.next()			# 执行next()方法,实现买入/卖出逻辑
    
    def next(self):
        # 判断是否是调仓日
        if self.is_adjust_day():
        
            # 如果还没有买入ETF仓位,则买入。
            if not self.getposition(self.datas[0]) :
                order = self.buy(self.datas[0], size=self.params.etf_size)
                order.addinfo(ticker=self.datas[0]._name)	
            
            # 如果已经持有期权仓位,则平仓。
            for d in self.datas[1:]:
                if self.getposition(d):
                    # 平掉已持仓期权
                    order = self.buy(d, size=self.params.etf_size)
                    order.addinfo(ticker=d._name)
                    
            # 获取比现价高两档的认购期权
            opt = self.get_opt(otype='call', pos=2, when=1)
            if opt:
                # 卖出期权
                d = self.getdatabyname(opt)
                order = self.sell(d, size=self.params.etf_size)
                order.addinfo(ticker=opt)
            else:
                print('没有找到可以卖出的期权。')
            
        return

	# 订单状态变化时引擎会调用notify_order
    def notify_order(self, order):
    
        if order.status in [order.Submitted, order.Accepted]:
            return

        # 如果交易已经完成,显示成交信息
        if order.status in [order.Completed]:
            if order.isbuy() or order.issell():
                print('{}  Buy/Sell {}, Price: {:.4f}, Size: {:6.0f}, Cost: {:.4f}, Comm {:.4f}'.format(
                        self.datetime.date(),
                        order.info['ticker'],
                        order.executed.price,
                        order.executed.size,
                        order.executed.value,
                        order.executed.comm)
                )

        # 如果订单未成交则给出提示
        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            print('Order Canceled/Margin/Rejected: {}'.format(order.info['ticker']))
    
    
    def is_adjust_day(self, dom=1):
        '''
        判断是否是每月的调仓日。        
        :params int dom: 每月第几个交易日进行调仓,缺省是第1个交易日。
        :return: 如果是调仓日,返回True,否则返回False。
        '''
        
        ret = False
        today = self.datetime.date()
        
        if self.month is None or self.month != today.month:
            self.month = today.month
            self.num_of_days = 1
        else:
        	self.num_of_days += 1
            
        if self.num_of_days == dom:
            ret = True
            
        return ret

    def get_opt(self, otype='call', pos=1, when=1):
        '''
        根据ETF当前价格获取期权。
        
        :params str otype: 期权类型,call或者put。
        :params int pos: 期权的位置,正数表示比当前标的价格高几档,负数表示比当前期权价格低几档。
        :params int when: 期权的到期日期,0/1/2/3分别表示当月/下月/当季/下季。
        :return: 期权代码,如果没有找到则返回None。
        '''
        
        etf_price = self.datas[0].close[0]
        
        # 获取期权的到期日期
        m = self.get_maturity(when=when)
        
        # 筛选这个到日期的期权并按照行权价由低到高排序
        d = self.params.opts
        d = d[ (d['maturity'] == m) & (d['type'] == otype) ]
        d = d.sort_values(by=['strike'])
        
        # 建立一个按照行权价由低到高排列的期权代码列表
        option_codes = []
        pos_etf = 0
        for _, row in d.iterrows():
            if row['strike'] >= etf_price :
                if pos_etf == 0 :
                    option_codes.append(None)
                    pos_etf = len(option_codes) - 1
                option_codes.append(row['code'])
        
        # 返回需要的期权代码
        idx = pos_etf + pos
        if idx >=0 and idx < len(option_codes) :
            return option_codes[idx]
        else:
            return None

    def get_maturity(self, when=1):
        '''
        获取期权的结束日期
        
        :param int when: 哪一个到期日期。0/1/2/3分别表示当月/下月/当季/下季的到期日期。
        :return: 期权的到期日期
        '''
        
        # 获取所有已经开始交易的期权代码
        trading_codes = []
        for d in self.datas:
            if len(d) > 0:
                trading_codes.append(d._name)

		# 选出到期日期大于等于今天的期权合约
        df = self.params.opts
        df = df[ df['maturity'] >= pd.to_datetime(self.datetime.date()) ]

		# 现在可以交易的期权的到期日期列表,按照从小到大排序
        m_list = sorted(list(set(df[df['code'].isin(trading_codes)]['maturity'])))

		# 如果给的参数不符合要求,返回最后一个日期
		if when > len(m_list):
			when = len(m_list) - 1
       
        return m_list[when]

在这个策略类中,多了几个新的方法。这里略微解释一下:

  • notify_order
    每次订单状态发生变化时,Backtrader引擎会调用这个方法。对于期权交易来说,最好重载这个方法来跟踪订单的执行情况。因为期权的成交量比较小,很可能会发生订单提交了但并没有成交的情况。

  • is_adjust_day
    自己定义的方法,判断当前交易日是否是调仓日。

  • get_opt
    自己定义的方法,根据期权的类型和价格位置来选择期权。比如本例中使用比现价高两档的认购期权。

执行策略

执行策略的代码如下:

# 初始化回测引擎
cerebro = bt.Cerebro()

# 设置交易资金和交易费用
cerebro.broker.set_cash(50000)
cerebro.broker.setcommission(commission=0.002)

# 添加自己编写的策略,opts是第1小节“策略所需数据”中提到的期权合约信息
cerebro.addstrategy(CoveredCallStrategy, opts=opts)

# 添加ETF日线数据到回测引擎。ETF是159919。日线数据在策略中通过self.datas[0]来引用,
data = PandasData(dataname=etf, datetime='date')
cerebro.adddata(data, name='159919') 

# 添加期权数据到回测引擎
for opt in list(set(df['code'])):
    d = df[df['code']==opt].iloc[:,1:]
	d.index = pd.to_datetime(d['date'])
    data = PandasData(dataname=d)
    cerebro.adddata(data, name=opt)
    
# 执行策略
cerebro.run()

# 设置回测结果中不显示期权K线
for d in cerebro.datas:
    d.plotinfo.plot = False
    
# 显示策略运行结果
cerebro.plot()

从2020.5.1至2020.7.17日,账户总价值从50,000增加到54,643.79,总收益9.3%。回测结果如下图:
在这里插入图片描述

相比之下,单纯持有ETF的收益有13%。显然在上涨期间使用covered call会减少收益。

回测结果默认会显示每个交易品种(data feed)的K线。我设置不显示K线,原因是参加回测的期权品种太多,导致显示时间很长,而且太多品种的数据叠加在一起看上去也很不方便。

添加分析指标

在回测的时候,除了最终的收益以外,通常还关心一些其它的指标,比如最大回撤,夏普比率等。这可以通过添加分析器(analyzer)来实现:

# 在执行策略之前添加分析器,我添加了3个,分别是:收益,回撤和夏普比率
cerebro.addanalyzer(bt.analyzers.Returns, _name='treturn')
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe')    
    
# 执行回测
thestrats = cerebro.run()

# 获取分析结果
thestrat = thestrats[0]
print(thestrat.analyzers.treturn.get_analysis())
print(thestrat.analyzers.drawdown.get_analysis())
print(thestrat.analyzers.sharpe.get_analysis())

小结

现在我们完整地实现了一个简单的期权回测策略。更进一步的工作可以考虑测试更加复杂的期权策略,或者也可以下载期权的分钟线来执行更细粒度的回测。

评论 25
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值