社区所有版块导航
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实践

中金点睛 • 2 年前 • 658 次点击  


转债数据库规范与统计案例: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/137596
 
658 次点击