社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

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

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

杨  冰分析员,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
 
1889 次点击