查看原文
其他

20行Python搞定可转债回测

爱捡便宜的交易员 野生交易员的试炼之路 2022-11-25

最近有几位朋友对python回测感兴趣,于是抽时间写了个教程。本文基于自己的粗浅理解,不能保证完全正确,有错误的地方欢迎指出。


我们常见的回测框架分两类,一类是事件驱动的,这类回测对开仓点、平仓点要求比较精细,一般要用到分钟级的K线数据,要求高的甚至要用tick级别的数据,回测过程需要对历史数据进行逐条遍历,相当于对交易环境做了一遍仿真模拟,这类回测精度较高,计算量较大,一般适用于周期较短、对交易成本较敏感的策略。第二类是通过矩阵运算直接得出答案,优点是计算量小,对数据要求低;缺点则是计算过程不够精确,无法处理精细的开平仓细节;适用于日线以上周期,对交易成本不敏感的策略回测。本文要介绍的简单方法属于第二种。


一般地,我们可以把一个策略回测概化成一个简单模型,输入是收益率矩阵和策略信号矩阵,输出则是策略的收益序列。对于任何一个策略,我们只要找到投资标的的历史数据,算出收益率矩阵和信号矩阵,就可以简单地得出收益序列。下面以双低转债的回测为例,做一遍实操。


首先,我们需要找到可转债的历史数据,数据里必须包括价格、溢价率,怎么获取历史数据不是本文的重点,我们先假定已经有了本地存储的历史数据,只需做个加载。

import pandas as pddata=pd.read_pickle('convdata_0521.pkl')

加载出来的数据长这样,日期是20080102至20210521:

我们要用到的关键的4列分别是转债代码、交易日期、收盘价、转股溢价率,这几项信息已经足够我们回测的了。


接下来需要做的是构造一个收益率矩阵,列名为转债代码,行号为交易日期。我们首先来构造一个收盘价的矩阵

pricedf=data[['tickerBond','tradeDate','closePriceBond']]pricedf=pricedf.set_index(['tradeDate','tickerBond']).unstack()['closePriceBond']pricedf.index=pd.to_datetime(pricedf.index)

构造好的价格矩阵长这样:


注意,这里面有很多数据为NaN,这是因为在这些日期这些转债还没上市或已退市。


接下来用价格矩阵来构造收益率矩阵,直接用DataFrame的pct_change函数就可以了。

day_return=pricedf.pct_change().shift(-1)

这里要注意,我算完之后用shift(-1)往前移了一天,这样算出来的2021年5月20日的收益率实际上对应的是20日收盘到21日收盘之间的收益率,这是因为我们在后面的计算中算出的策略信号矩阵是用20日的数据求得20日盘尾的信号,我们如果在20日盘尾按此信号买入的话那么实现的收益率正好对应后一天的收益率,这样我们在后面计算的时候就实现了日期对齐了。


下面要做的就是构造信号矩阵,信号矩阵要表达的信息是基于每个日期的收盘数据求得的下个日期的持仓信息。其中列名代表的是转债代码,行号代表的是信号计算日期。我们假设等权买入,每个单元格都用0和1来区分是否买入。由于我们在收益率矩阵中已经处理了日期对齐问题,这里日期就不用再处理了。计算信号矩阵前,我们先要计算双低因子,即双低=价格+溢价率。价格矩阵我们前面已经构造好了,同样的方法来构造溢价率矩阵。

premdf=data[['tickerBond','tradeDate','bondPremRatio']]premdf=premdf.set_index(['tradeDate','tickerBond']).unstack()['bondPremRatio']premdf.index=pd.to_datetime(premdf.index)

然后求出双低因子

factor=premdf+pricedf

接下来,就可以通过筛选每日最低的20个转债来得到信号矩阵了,这个过程稍微需要用到点技巧。

N=20def selectTopN(tmp): tmp=tmp.copy() symbols=tmp.nsmallest(N).index tmp[:]=0 tmp[symbols]=1 return tmpsignal=factor.apply(selectTopN,axis=1)

终于,我们得到了信号矩阵,全是零是不是有点慌?不要着急,因为总共几百列里只有20个1,每行里的1其实都是很稀疏的,我们随便找一列来验证一下。

row=signal.iloc[-1]row[row>0]

没错,不多不少正好20个。最好把这个持仓列表的双低值再人工校验一遍,保证我们的信号计算是正确的,这里就先跳过此步。


接下来终于到了激动人心的收益序列计算了,其实这步是最简单的,直接两个矩阵乘一下,注意这里并不是线性代数里面的矩阵乘法,而是两个矩阵的相同位置的数字直接相乘。然后将每行的结果求和除以20,就得到收益率序列了。

pnl=(signal*day_return).sum(axis=1)/N

注意,这里得出21日的收益率是0,这是正确的,因为我们在日期处理时将收益率的时间对齐到了信号的时间,21日产生的信号需要22日的数据才能知道收益。之所以要反复强调时间对齐问题,是因为一旦时间没弄对,就会引入未来函数,造成收益率超高的假象。

到这里,计算就已经完成了,我们还可以画个净值曲线来直观地看下回测结果,只要把每日的收益率加1后连乘就可以了。

(pnl+1).cumprod().plot(figsize=(10,5),grid=True)


好了,一个简单的按日调仓的双低转债策略就回测完成了,我数了一下,也就20行代码,真的是非常简单。为了方便你们复制粘贴,我把它们再合起来放一遍:

import pandas as pd
data=pd.read_pickle('convdata.pckl')pricedf=data[['tickerBond','tradeDate','closePriceBond']]pricedf=pricedf.set_index(['tradeDate','tickerBond']).unstack()['closePriceBond']pricedf.index=pd.to_datetime(pricedf.index)day_return=pricedf.pct_change().shift(-1)premdf=data[['tickerBond','tradeDate','bondPremRatio']]premdf=premdf.set_index(['tradeDate','tickerBond']).unstack()['bondPremRatio']premdf.index=pd.to_datetime(premdf.index)factor=premdf+pricedf
N=20def selectTopN(tmp): tmp=tmp.copy() symbols=tmp.nsmallest(N).index tmp[:]=0 tmp[symbols]=1 return tmp
signal=factor.apply(selectTopN,axis=1)pnl=(signal*day_return).sum(axis=1)/N(pnl+1).cumprod().plot(figsize=(10,5),grid=True)

最后再加点难度,如果我不想每天调仓,我想5天调一次仓,怎么做呢?这里要对信号矩阵做个采样,即每5天才算一次信号,稍微要一点技巧,直接给出代码和结果:

tmpdf=signal.iloc[range(0,len(signal),5)]week_selected_df=pd.DataFrame(index=signal.index)week_selected_df=week_selected_df.join(tmpdf)week_selected_df=week_selected_df.fillna(method='pad')
pnl=(week_selected_df*day_return).sum(axis=1)/N(pnl+1).cumprod().plot(figsize=(10,5),grid=True)


好了,我感觉我已经写的够详细了,有进一步沟通需求的可以私信加我微信。


最后再送个大礼包,本文回测用到的原始数据见附件。


最后的最后,尝试开通了赞赏功能,觉得有帮助的朋友给个鼓励呗。


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存