Py学习  »  Python

【中金固收·可转债】转债数据库规范与统计案例:Python实践

中金固定收益研究 • 1 年前 • 690 次点击  


转债数据库规范与统计案例:Python实践


相比于标准的股和债,转债的数据有既复杂又标准的特点。复杂在于,转债不仅涉及自身的诸多指标,还要与正股关联,同时还有条款等需要处理的问题。标准则是相对于纯债而言,转债的交易、估值等数据更接近于方便标准化的状态。更重要的是,与标准的软件开发非常不同,转债投研所用的数据库应当是高度定制化的,相比于软件设计的规范性来说,易用、实用更加重要。 我们从数据库开始介绍实践中的数据处理方式。


集成变量设计

首先,我们需要一个集成变量,而非散落各处的数据表。数据存在的意义,在于后续要用于统计和策略设计,散落于多处的状态将造成诸多不便。因此我们设计一个基础的class变量,用于装载、调用数据,并在这里就实现一些常用的功能。


图表1:基础class变量

class cb_data(object):

    def __init__(self):
        self.DB = {} # DB为字典,准备装载各维度的数据
        self.loadData() # loadData后续定义

资料来源:中金公司研究部


对于每一个具体字段,csv文件 + DataFrame或是较好的一种实现载体。这里不选择Pickle是因为csv更方便用Excel等打开查看,符合习惯。就日频数据而言,至少价格、成交额,以及溢价率、剩余期限、规模等数据是需要的。我们在下表中列明日频数据中,我们明确需要的数据名称、终端字段、存储文件名称以及默认的统计方式(便于日常进行统计,例如溢价率常用平均、余额常用加总)。我们将下表命名为"参数.xlsx"以便调用。


图表2:参数表格

资料来源:Wind,同花顺,中金公司研究部


于是我们可以将上述的loadData具体化为下面的形式。这里最后我们也要载入一些静态数据(如条款),将在后文补充:


图表3:读取本地参考数据

def loadData(self):

    self.dfParams = pd.read_excel("参数.xlsx", index_col=0)
    for k, v in self.dfParams.iterrows():
        df = pd.read_csv(v["文件名"], index_col=0)
        df.index = pd.to_datetime(df)
        self.DB[k] = df
        
    self.panel = pd.read_excel("静态数据.xlsx", index_col=0, encoding="gbk")

资料来源:中金公司研究部


实践中如果每次都要从cb_data.DB去调用每个表的数据,会略显繁琐。此处我们重载了数据引用的方式(getitem和getattr),如下。此外,由于常常要用到转债代码列表(包含全体转债的代码)和最新交易日,我们也定义了简便的提取方式。这里使用了property装饰器,以便未来直接用data或者codes调用。以及我们也定义一个codes_active以方便获取当前(最后一个交易日)仍在交易的转债代码。


图表4:调取基础数据的方式

def __getitem__(self, key):
    return self.DB[key] if k in self.DB.keys() else None
def __getattr__(self, key):
    return self.DB[key] if k in self.DB.keys() else None

@property
def date(self):
    return  self.DB["Amt"].index[-1]
@property
def codes(self):
    return list(self.DB["Amt"].columns)
@property
def codes_active(self):
    srs = self.DB["Amt"].loc[self.date, self.codes]
    return list(srs[srs > 0].index)

资料来源:中金公司研究部



数据获取和更新

上述都建立在我们本就有存储数据的文件的基础上。自然,我们需要数据初始化以及更新的功能。实际提取数据表并不难,但在此之前我们需要找到我们感兴趣的转债的列表。一个简易的实践方式,是提取某个年份(例如2015年)后的发行列表,然后需要再剔除私募品种。这里我们用两个外部函数(而非在class内部)来实现,如下(以Wind为例)。这样一来我们便可以用readTable来完成各日频数据的初始化。


图表5:基础数据读取的初始化

def getCodeList():
    if not w.isconnected(): w.start()
    _ , dfIssue = w.wset("cbissue", "startdate=2015-01-01;enddate=2022-12-31", usedf=True)
    return list(dfIssue.loc[dfIssue["issue_type"] != "私募"].index)

def readTable(codes, field, start, end, *others):
    _, df = w.wsd(",".join(codes), field, start, end, others, usedf=True)
    return df

资料来源:中金公司研究部



相比于只执行一次的初始化,数据库的更新可能更为重要。我们需要一个通用函数来执行每个表的更新,并在class中集成所有变量的更新。


图表6:数据读取的通用函数

def tblUpdate(df, end, field, method="wind-api"):
    # end为截止日期
    # method是为其他数据接口如同花顺、SQL库等,这里不展开
    codes = df.columns
    dates = w.tdays(df.index[-1], end).Data[0]
    
    if len(dates) > 1:
        kwargs = "rfIndex=1" if field == "impliedvol" else None
        dfNew = readTable(codes, field, dates[1], dates[-1], kwargs)
        df = df.append(dfNew)
        return df
    else:
        print("不用更新")
        return df
# 以下在cb_data内部
def update(self, end, method="wind-api"):
    for k, v in self.dfParams.iterrows():
        df = self.DB[k]
        df = tblUpdate(df, end, v["字段(Wind)", method])
        self.DB[k] = df
        print(f'{k} 更新已完成')

资料来源:中金公司研究部



如何加入新发的转债?本质上也是数据表的扩展,只是若有新券,需要实现的是横向扩展,而非纵向。当然,考虑到数据量问题,此处更建议投资者利用SQL等本地数据源来实现。下面的method字段,也是主要为此保留。最后的panelData主要用以更新条款、评级等新数据。



图表7:新券纳入的函数

def insertNewKey(self, new_codes, method='wind-api'):
    
    for key,value in self.DB.items():
       
        diff = list(set(new_codes) - set(self.DB.keys()))
                                                                                            
        if diff:
                field = self.dfParams.loc[key, '字段(Wind)']
    
                start = self.DB[key].index[0] ; end = self.DB[key].index[-1]
                
                if method == "wind-api":
                    kwargs = "rfIndex=1" if field == "impliedvol" else None
                    df = readTable(diff, field, start, end, kwargs)
                    
                value = value.join(df)

                self.DB[key] = value
                    
        self.updatePanelData(new_codes)

资料来源:Wind,中金公司研究部



非日频数据

这里主要指一些面板类数据,例如转债的等级、条款、发行人行业等,它们不会随着时间而变化。为方便查看,我们希望将其集成在一个表内,且输出为Excel格式即可。下面是一些我们认为会比较常用的字段:


图表8:面板数据参照列表

资料来源:中金公司研究部



调取和存储则十分简单,对其更新也可以通过panelData直接简单并入“insertNewkey”,此处不赘述。



图表9:面板数据读取及更新

def readPanel(self, codes=None):

    date = pd.to_datetime(self.date).strftime("%Y%m%d")
    if codes is None: codes = self.codes

    dfParams = pd.read_excel("静态参数.xlsx", index_col=0, encoding="gbk")

    _, df = w.wss(codes, ",".join(dfParams["字段(Wind)"]),
                  f"tradedate={date}", usedf=True)

    df.columns = list(dfParams.index)

    return df

def updatePanelData(self, new_codes=None):

    if new_codes is None: new_codes = self.codes

    diff = list(set(new_codes) - set(self.panel.index))

    if diff:
        dfNew = self.readPanel(diff)
        self.panel = self.panel.append(dfNew)

资料来源:中金公司研究部



另有一些“准静态”数据:例如赎回公告日、持有人结构等,虽然并非经常变化,这些数据我们更建议时用时取即可。


如何完成一些常用的统计

例如估值指标,我们常求均值或中位数等。但当然,当日没有交易的转债不必纳入其中。以及,近年来的“双高”也会大面积干扰结果。因此,“是否交易”以及“是否非异常”会是非常常用的两个矩阵,我们有必要在cb_data中预置。这里仍沿用property装饰,以便使用。


图表10:样本选择相关的函数

@property
def matTrading(self):
    return self["Amt"].applymap(lambda x: 1 if x > 0 else np.nan)


@property
def matNormal(self):
    
    matTurn = self.DB["Amt"] * 10000.0 / self.DB["Outstanding"] / self.DB["Close"]
    
    matEx = (matTurn.applymap(lambda x: 1 if x > 100 else np.nan) * \
         self.DB["Close"].applymap(lambda x: 1 if x > 135 else np.nan) * \
         self.DB["ConvPrem"].applymap(lambda x: 1 if x >35 else np.nan
                             )).applymap(lambda x: 1 if x != 1 else np.nan)
    
    return self.matTrading * matEx

资料来源:中金公司研究部


有了这些准备,进行一些常用统计就很简单了。下面三个案例作为参考,分别统计转债平均价格、平价在90~110元的转债平均溢价率、10日平均移动隐含波动率。同其他数据处理技巧一样,我们也尽量避免进入循环,尽可能地利用矩阵运算。对Python不熟悉的投资者要关注apply与applymap的差异。


图表11:指标计算函数

obj2 = cb_data()

# 求均价,非异常样本
(obj2.matNormal * obj2.Close).apply(np.mean, axis=1)

# 求平价90~110元转债平均溢价率
(obj2.matNormal * obj2.ConvV.applymap(lambda x: 1 if 90 <= x < 110 else np.nan) *\
obj2.ConvPrem).apply(np.mean, axis=1)

# 求10日平均隐含波动率
(obj2.matNormal * obj2.ImpliedVol).apply(np.mean, axis=1).rolling(10).mean()

资料来源:中金公司研究部



文章来源

本文摘自:2022年7月15日已经发布的《转债数据库规范与统计案例:Python实践


杨 冰 SAC执业证书编号:S0080515120002;SFC CE Ref: BOM868

房 铎 SAC执业证书编号:S0080519110001

罗 凡 SAC执业证书编号:S0080522070003 

陈健恒 SAC执业证书编号:S0080511030011;SFC CE Ref: BBM220


法律声明

向上滑动参见完整法律声明及二维码


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