失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > 利用Backtrader进行期权回测之四:Covered Call策略

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

时间:2024-02-13 10:14:48

相关推荐

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

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

策略所需的数据

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

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

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

沪深300ETF数据:

期权日线数据:

期权合约数据:

策略类实现

下面是完整的CoveredCallStrategy代码:

import pandas as pdimport backtrader as btfrom backtrader.feeds import PandasData# Covered Call 策略class CoveredCallStrategy(bt.Strategy):params = (('opts', None),# 期权合约信息('etf_size', 10000),# 每手期权对应的基金份数)def __init__(self):self.month = Noneself.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_orderdef notify_order(self, order):if order.status in [order.Submitted, order.Accepted]:return# 如果交易已经完成,显示成交信息if order.status in [pleted]: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,m))# 如果订单未成交则给出提示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 = Falsetoday = self.datetime.date()if self.month is None or self.month != today.month:self.month = today.monthself.num_of_days = 1else:self.num_of_days += 1if self.num_of_days == dom:ret = Truereturn retdef 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.optsd = d[ (d['maturity'] == m) & (d['type'] == otype) ]d = d.sort_values(by=['strike'])# 建立一个按照行权价由低到高排列的期权代码列表option_codes = []pos_etf = 0for _, row in d.iterrows():if row['strike'] >= etf_price :if pos_etf == 0 :option_codes.append(None)pos_etf = len(option_codes) - 1option_codes.append(row['code'])# 返回需要的期权代码idx = pos_etf + posif idx >=0 and idx < len(option_codes) :return option_codes[idx]else:return Nonedef 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.optsdf = 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) - 1return 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()

从.5.1至.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())

小结

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

如果觉得《利用Backtrader进行期权回测之四:Covered Call策略》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。