Py学习  »  Python

【中金固收·可转债】简易的转债策略测试框架——以及python实现方法 20190519

中金固定收益研究 • 4 年前 • 1203 次点击  
作者

杨  冰分析员,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.DataFrameindexyyyy/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)

 

    # 进入循环,enumeratepython里面一个很好用的迭代函数

    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

 

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


本文所引为报告部分内容,报告原文请见2019519日中金固定收益研究发表的研究报告

相关法律声明请参照:

http://www.cicc.com/portal/wechatdisclaimer_cn.xhtml



Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/33264
 
1203 次点击