相比于标准的股和债,转债的数据有既复杂又标准的特点。复杂在于,转债不仅涉及自身的诸多指标,还要与正股关联,同时还有条款等需要处理的问题。标准则是相对于纯债而言,转债的交易、估值等数据更接近于方便标准化的状态。更重要的是,与标准的软件开发非常不同,转债投研所用的数据库应当是高度定制化的,相比于软件设计的规范性来说,易用、实用更加重要。 我们从数据库开始介绍实践中的数据处理方式。
首先,我们需要一个集成变量,而非散落各处的数据表。数据存在的意义,在于后续要用于统计和策略设计,散落于多处的状态将造成诸多不便。因此我们设计一个基础的class变量,用于装载、调用数据,并在这里就实现一些常用的功能。
class cb_data(object):
def __init__(self):
self.DB = {} # DB为字典,准备装载各维度的数据
self.loadData() # loadData后续定义
对于每一个具体字段,csv文件 + DataFrame或是较好的一种实现载体。这里不选择Pickle是因为csv更方便用Excel等打开查看,符合习惯。就日频数据而言,至少价格、成交额,以及溢价率、剩余期限、规模等数据是需要的。我们在下表中列明日频数据中,我们明确需要的数据名称、终端字段、存储文件名称以及默认的统计方式(便于日常进行统计,例如溢价率常用平均、余额常用加总)。我们将下表命名为"参数.xlsx"以便调用。
于是我们可以将上述的loadData具体化为下面的形式。这里最后我们也要载入一些静态数据(如条款),将在后文补充:
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以方便获取当前(最后一个交易日)仍在交易的转债代码。
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来完成各日频数据的初始化。
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中集成所有变量的更新。
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主要用以更新条款、评级等新数据。
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)
这里主要指一些面板类数据,例如转债的等级、条款、发行人行业等,它们不会随着时间而变化。为方便查看,我们希望将其集成在一个表内,且输出为Excel格式即可。下面是一些我们认为会比较常用的字段:
调取和存储则十分简单,对其更新也可以通过panelData直接简单并入“insertNewkey”,此处不赘述。
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装饰,以便使用。
@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的差异。
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