来源:中金固定收益研究

作者

杨  冰分析员,SAC执业证书编号: S0080515120002

房  铎联系人,SAC执业证书编号: S0080117080049

姬江帆分析员,SAC执业证书编号:S0080511030008;SFC CE Ref: BDF391

策略好不好,测了才知道 —— 但对于转债来说,可能没那么容易。虽然转债已经基本告别当年迷你市场的窘境,但依然是一个小市场,这是我们在年度回顾报告中的一个判断 —— 而附带的一个影响是,很多配套的东西还不完备,比如策略测试的代码框架。幸好开源的理念之下,这些事情自行处理也并不太复杂。我们在此介绍一个简单的测试框架及其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

上面提到的这几个指数如下图:

  • 01 / 刘欣跟Trish算人均GDP账 追问如何定义发达国家?
  • 02 / 刘欣 :知识产权保护是中国社会各界的共识
  • 03 / 股价闪崩 加拿大鹅“折翼”
  • 04 / 6名中国富豪承诺捐一半财产:含蒙牛创始人牛根生
  • 05 / 北京房租变便宜了?房东若要求高价 房屋空置时间更长
  • 06 / 庞氏青年水氢汽车梦:多看一眼会知道那就是一个局
  • 07 / 联邦快递入华35年无存在感 股价一年多跌超四成
  • 08 / 从钱学森之问到任正非之问:拔尖学生能否培养成大师?
  • 09 / 全国50城租房压力榜:北上深月入收入几乎用来付房租
  • 10 / 地方国企改革入深水区 上海两国企改革机构合二为一
  • 01 / 方大特钢爆炸伤亡惨重 富豪掌控700亿常堆钱山炫富
  • 02 / 相当于损失166万件羽绒服!昨晚 加拿大鹅突然崩盘了
  • 03 / 美欧贸易谈判陷入僵局 新一轮战火即将燃起
  • 04 / 5月30日上市公司晚间公告速递
  • 05 / 中兴股东大会 总裁徐子阳回应华为被列入实体清单
  • 06 / 科创板第二批上会名单揭晓 最全影子股名单看这里
  • 07 / 南昌方大特钢爆炸最新消息,南昌方大特钢爆炸严重吗原因是什么
  • 08 / 十大博客看后市:底部已经明确 越跌越是机会
  • 09 / 5月31日涨停板早知道:七大利好有望发酵
  • 10 / 最早7月!美国恐将对欧盟加征关税
  • 01 / 阎庆民:下一步将探索创立科转板
  • 02 / 网贷陷阱:1万贷款半年变成400万 永远还不完钱的套路
  • 03 / 易纲:确保实现国有行小微贷款余额增长30%以上目标
  • 04 / 王兆星:进一步扩大外资行、保险机构经营范围和空间
  • 05 / 5月30日在售高收益银行理财产品一览
  • 06 / 易纲:推动外资、外汇政策在金融街先行先试
  • 07 / 乱扫二维码后果很严重 谨防二维码被盗刷
  • 08 / 闫庆民:创新金融产品 丰富服务经济高质量发展新工具
  • 09 / 网贷业连续十月无新增平台 预计年底将剩下不足500家
  • 10 / 招行钱端陷入"口水仗" 谁背14亿逾期资产的锅?
  •