策略好不好,测了才知道 —— 但对于转债来说,可能没那么容易。虽然转债已经基本告别当年迷你市场的窘境,但依然是一个小市场,这是我们在年度回顾报告中的一个判断 —— 而附带的一个影响是,很多配套的东西还不完备,比如策略测试的代码框架。幸好开源的理念之下,这些事情自行处理也并不太复杂。我们在此介绍一个简单的测试框架及其Python实现方法。
首先还是看大的框架,然后再一步一步完成细化实现。大体的流程应该包括:
1、初始化定义:测试的时间段、考虑的转债的范围(比如含不含EB、含不含那些因股改而退市的品种)、调仓周期、以及最终的返回值——策略的净值和必要的记录;
2、进入测试循环:计算净值、并在调仓的时点上进行调仓;
3、返回结果。此时的Python代码如下:
# 引入三个必须引用的库
import datetime as dt
import pandas as pd
import numpy as np
def frameStrategy(obj, start='2015/12/31'):
'''
这里的参数还不完全,为了简单先只留最简单的两个
obj是我们自己设定的一个class,进行日常的转债数据维护和计算,不过此时投资者不必太过在意,因为后面我们将只用其作为数据库的功能
obj.DB
例如 obj.DB['Amt']将返回一个记录转债成交额的pd.DataFrame,index是yyyy/mm/dd型的日期,columns是各转债的代码
'''
# 设定起始日期在库中的位置(我们的数据从2002年开始,这里要返回一个整数,记录start在其中的位置,比如2015/12/31对应的是3391)
# 这个getStartLoc将在后面介绍,后面还有很多这类函数
intStart = getStartLoc(obj, start)
# dfRet是最终要返回的表,'NAV'这一列就是最重要的了:策略净值(我们这里是100为起点)
dfRet = pd.DataFrame(index=obj.DB['Amt'].index[intStart:],columns=['NAV','LOG:SEL','LOG:WEIGHT'])
# 这个表记录了持仓,index是转债代码,初始先设定成[Nothing]
dfAssetBook = pd.DataFrame(index=['Nothing'],columns=['costPrice', 'w'])
# 需要一个变量来记录持仓中的现金(或者借款)
cash = 100.0
# 设定转债代码范围
codes = defineCodes(obj, defineMethod)
# 一个调仓的日期列表,这里设定的是每
21个交易日调仓一次
isAdjustDate = roundOfAdjust(obj, start, 21)
# 进入循环,enumerate是python里面一个很好用的迭代函数
for i,date in enumerate(dfRet.index):
# 这一步来记录净值变化
checkBook(obj, dfRet, dfAssetBook, cash,date)
# 判定当日是否需要调仓
if date in isAdjustDate:
# 如果需要调仓,进入selectCodes函数,根据策略选择个券
sel = selectCodes(obj, codes, date, selMethod)
if sel:
# 这一步得到权重变量
w = getWeight(obj, sel, date, weightMethod)
else:
sel = ['Nothing']
w = 0.0
dfAssetBook = pd.DataFrame(index=sel, columns=['costPrice', 'w'])
dfAssetBook['costPrice'] = 100.0
dfAssetBook['w'] = w
# 无论如何,都用dfRet来记录当日持仓的个券和权重
# join函数非常实用,用来连接字符串
dfRet['LOG:SEL'][date] =','.join(list(dfAssetBook.index))
# [func(t) for t in ...] 是非常具备python特色的一个处理方法
dfRet['LOG:WEIGHT'][date] =','.join([str(t) for t in list(dfAssetBook['w'])])
return dfRet
下面来逐个击破中间的小函数。首先是getStartLoc,实际上pd.DataFrame的index有一个get_loc的方法也能得到这个结果,但早期的版本没考虑过万一要找的变量不在index中怎么办。而后来的版本中,虽然给予了一定容忍度,但也基本没考虑过当index本身是不可比变量时的处理。所以此时我们要进行简单的改造,如下:
def getStartLoc(obj,date):
# 如果get_loc能解决,就交给它吧
if date in obj.DB['Amt'].index:
i= obj.DB['Amt'].index.get_loc(date)
else:
# 如果解决不了,要先把index转化成datetime型,而非原本的字符型,这样get_loc就能万用了
fakeIndex = obj.DB['Amt'].index.map(str2dt)
i= fakeIndex.get_loc(str2dt(date),method='ffill')
return i
接下来是定义个券大致范围的defineCodes:一般要剔除因股改而退市的那些转债,有时候我们也希望剔除EB。投资者也可以设定其他的规则,这就需要用到一个python特性:函数可以作为参数传入另一个函数。这样的话,投资者可以自行编写一个函数,作为定义范围的方法。实现如下:
def defineCodes(obj,method='default'):
if method== 'default':
return obj._excludeSpecial()
elif method== 'nonEB':
return
obj._excludeSpecial(hasEB=0)
elif hasatrr(method,’__call__’): # 这一句是判断method是不是一个函数,如果是,则调用这个函数
return method(obj)
# _excludeSpecial() 是我们的obj中的方法,如下:
def _excludeSpecial(self,hasEB=1):
columns = set(list(self.DB['Amt'].columns))
# 这个cb_data.lstSpecial里面存了那些因股改而退市的转债的代码
columns -= set(cb_data.lstSpecial)
columns = list(columns)
# 如果不要EB,进入下面的程序
if not hasEB:
for code in columns:
if code[:3] == '132' or code[:3] == '120':
columns.remove(code)
return columns
下面是择券的代码,也是对策略决定意义最大的函数。在调仓日期会调用这个函数。同样,为了给予投资者外部接口,这里也要保留传入函数的可能性。如下:
def selectCodes(obj, codes, date,selMethod=None):
i = getStartLoc(obj,date)
n = min([i,5])
# 这里利用一下pandas.DataFrame的逻辑运算做最基本的条件设定:前5个交易日必须有最少10万的交易
# 且存量不低于3000万
condition = (obj.DB['Amt'].iloc[i-n:i][codes].fillna(0).min() >100000.0) & \
(obj.DB['Outstanding'].iloc[i][codes]> 30000000.0)
## 如果selMethod不为空
If selMethod:
tempCodes= list(condition[condition].index)
moreCon= selMethod(obj, codes, date, tempCodes)
condition&= moreCon
# 这个函数最后返回的变量是这个
retCodes = list(condition[condition].index)
# 如果一个都没有,进入这里,并给出提示
if not retCodes:
print 'its a empty selection, when date: ',date
return retCodes
# 下面以低价策略举例,如果我们希望在调仓时买入所有价格低于均价的品种,则可以写下面这个函数,并把_lowprice作为selMethod传入上面的函数:
def _lowPrice(obj, codes, date, tempCodes):
avgPrice = obj.DB['Close'].loc[date][tempCodes].mean()
return obj.DB['Close'].loc[date, codes] <= avgPrice
然后是转债初始权重的设定函数:我们可以预设几个常用的,比如等权、市值加权。投资者也可以自行设定,自然这也要依赖于传入一个函数参数,不过在加权这个上面,往往不用太多费精力:
def getWeight(obj, codes, date, method='average'):
if method == 'average':
# 等权策略
# 这里要依赖一下numpy中的ones了
ret = pd.Series(np.ones(len(codes))/ float(len(codes)),index=codes)
return ret
elif method == 'fakeEv':
# 按发行额加权,即“假市值”。中证转债指数类似这种
srsIssue = get_issueamount(codes)
srsFakeEv = obj.DB['Close'].loc[date,codes] * srsIssue
return srsFakeEv / srsFakeEv.sum()
elif method == 'Ev':
# 市值加权
srsOutstanding = obj.DB['Outstanding'].loc[date,codes]
srsEv = obj.DB['Close'].loc[date, codes] *srsOutstanding
return srsEv / srsEv.sum()
elif elif hasatrr(method,’__call__’) :
return method(obj, codes, date)
调仓周期函数:比较简单,不过这里我们只留了两种形式,一种是每日调仓(但这个其实没有想象中那么实用),另一种是每隔固定交易日调仓一次。实现如下:
def roundOfAdjust(obj, start, method='daily'):
i = getStartLoc(obj,start)
if method == 'daily':
return obj.DB['Amt'].index[i:]
elif isinstance(method,int): # 这里有一个值得注意,验证数据类型,不要用 type(data) == ...,而是instance
# [::n]就是每隔n个数取一次了
return obj.DB['Amt'].index[i:][::method]
最后是checkBook:也就是对于账簿的每日处理函数。这个内容比较简单,值得注意的是:1、这个函数没有任何返回值,但dfRet、dfAssetBook乃至cash都会被它改变,这是python的一个特性,可以多加利用;2、cash的意义是在于仓位不满或者超过100%时,记录现金的成本或者收益
def checkBook(obj, dfRet, dfAssetBook, cash, date,cashRate = 0.03):
if date == dfRet.index[0]:
dfRet.loc[date]['NAV'] = 100
else:
i = dfRet.index.get_loc(date); j = obj.DB['Close'].index.get_loc(date)
if len(dfAssetBook.index)== 1 and dfAssetBook.index[0] == 'Nothing':
dfRet.iloc[i]['NAV'] =dfRet.iloc[i-1]['NAV'] * (1 + cashRate/252.0)
cash *= 1 + cashRate / 252.0
else:
codes = list(dfAssetBook.index)
srsPct = obj.DB['Close'].iloc[j-1:j+1][codes].pct_change().iloc[-1] + 1.0
cashW = 1 - dfAssetBook['w'].sum()
t1 = (srsPct *dfAssetBook['costPrice'] * dfAssetBook['w']).sum()+ cash * cashW * (1 + cashRate)
t0 = (dfAssetBook['costPrice']* dfAssetBook['w']).sum() + cash * cashW
dfRet.iloc[i]['NAV'] =dfRet.iloc[i-1]['NAV'] * t1 / t0
cash *= 1 + cashRate
最后的最后,是对主函数frameStrategy的修正——因为输入参数绝不止obj和起始时间start。结合上面几个小函数的讨论,至少这几个参数是可以留给投资者自设的(当然沿用默认设置也没问题):1、defineCodes中的method,用来调整择券范围;2、selectCode中的method,用来调整核心策略;3、getWeight中的method,用来调整加权方法;4、调仓周期的参数。因此,这个函数的def行应该是这样的:
def frameStrategy(obj,start='2015/12/31',
defineMethod='default',
selMethod=None,
weightMethod='average',
roundMethod='daily')
投资者不仅可以用来测试策略,自定义指数也可以比较轻易地计算,而不用再依赖卖方提供的数据了 —— 而且相对于别人的数据,投资者会更清楚地理解自己编写的指数。比如上面的案例可以作为低价品种等权指数,稍作改动就可以变成“高价指数”。再如,将下面的函数作为selMethod传入框架,可以得到低溢价率品种指数:
def _lowPrem (obj, codes, date, tempCodes):
avgPrem= obj.DB['ConvPrem'].loc[date][tempCodes].mean()
return obj.DB[' ConvPrem '].loc[date, codes] <= avgPrem
上面提到的这几个指数如下图:
