由于数据获取容量上限为3000,时间跨度不宜太长。我们的研究时间跨度为2018.6.1~2019.6.1。篇幅所限,以下所有数据结果均展示最后5行。
#导入模块
from jqdata import *
import datetime as dt
import matplotlib.pyplot as plt
%matplotlib inline
import pandas as pd
import numpy as np
import random
import warnings
warnings.filterwarnings('ignore')
限售解禁的原因很多,不同的解禁类型对应不同类型的股东,这些股东对公司信息的掌握情况不同,故解禁类型是我们关注的一个方面。不妨先看一下各种解禁类型的数量统计情况。
#定义起始和终止日期
date1 = dt.date(2018,6,1)
date2 = dt.date(2019,6,1)
#NewMarketableSharesSource:解禁原因的文字说明
#SourceType:解禁原因代码
q = query(
jy.LC_SharesFloatingSchedule.SourceType,
jy.LC_SharesFloatingSchedule.NewMarketableSharesSource
).filter(jy.LC_SharesFloatingSchedule.StartDateForFloating>=date1,
jy.LC_SharesFloatingSchedule.StartDateForFloating<date2)
release = jy.run_query(q)
#展示最后5行
release.tail()
SourceType | NewMarketableSharesSource | |
---|---|---|
2136 | 24 | 增发A股法人配售上市 |
2137 | 80 | 股权激励限售流通 |
2138 | 24 | 增发A股法人配售上市 |
2139 | 82 | 发行前股份限售流通 |
2140 | 82 | 发行前股份限售流通 |
#统计各种解禁类型的占比
(release.groupby('NewMarketableSharesSource').count()/len(release)).applymap(lambda x: format(x, '.1%'))
SourceType | |
---|---|
NewMarketableSharesSource | |
A股发行法人配售上市 | 0.0% |
其他 | 0.1% |
发行前股份限售流通 | 23.8% |
增发A股原股东配售上市 | 11.3% |
增发A股法人配售上市 | 35.9% |
延长限售锁定期流通 | 2.0% |
股权分置股东增持股份上市 | 0.0% |
股权分置限售流通 | 1.4% |
股权激励限售流通 | 25.0% |
配股限售流通 | 0.3% |
在2141行数据中,“发行前股份限售流通”、“增发A股法人配售上市”和“股权激励限售流通”是占比前三的解禁类型,总占比近85%。因此,这3种解禁类型是我们研究的重点。又因为股票的解禁数量与未来的抛售压力直接相关,自然我们关注的另一个方面是解禁规模的大小。我们用当期解禁数量占解禁前流通数量的比例(以下简称“解禁比例”)来描述这一指标。下面获取上述3种解禁类型的明细数据,字段包括公司代码、解禁日期、解禁比例和解禁类型。
#研究3种类型的限售解禁(24-增发A股法人配售上市,80-股权激励限售流通,82-发行前股份限售流通)
types = [82,24,80]
#CompanyCode:公司代码
#StartDateForFloating:解禁日期
#Proportion1:本次解禁数量占上期末已流通数量比例(%)
q = query(
jy.LC_SharesFloatingSchedule.CompanyCode,
jy.LC_SharesFloatingSchedule.StartDateForFloating,
jy.LC_SharesFloatingSchedule.Proportion1,
jy.LC_SharesFloatingSchedule.SourceType,
).filter(jy.LC_SharesFloatingSchedule.StartDateForFloating>=date1,
jy.LC_SharesFloatingSchedule.StartDateForFloating<date2,
jy.LC_SharesFloatingSchedule.SourceType.in_(types),
).order_by(jy.LC_SharesFloatingSchedule.StartDateForFloating)
unbanned = jy.run_query(q)
#对解禁比例(%)保留两位小数
round(unbanned.tail(),2)
CompanyCode | StartDateForFloating | Proportion1 | SourceType | |
---|---|---|---|---|
1807 | 3955 | 2019-05-31 | 8.99 | 24 |
1808 | 75210 | 2019-05-31 | 0.24 | 80 |
1809 | 78120 | 2019-05-31 | 2.07 | 24 |
1810 | 170454 | 2019-05-31 | 377.54 | 82 |
1811 | 178293 | 2019-05-31 | 97.20 | 82 |
以上第1列‘CompanyCode’并非是我们常用的证券代码。为了便于研究,需要将它们转化为证券代码,方法是通过关联聚源数据的SecuMain表来实现。于是定义代码转换函数如下:
#定义函数:将公司代码转化为证券代码
def com2sec(code):
#若在任一张表中查不到数据,则返回空值nan
symbol = np.nan
#获取含有公司代码code的SecuMain表
q = query(
jy.SecuMain.CompanyCode,
jy.SecuMain.SecuAbbr).filter(jy.SecuMain.CompanyCode==code)
df1 = jy.run_query(q)
#先判断SecuMain表是否为空
if not df1.empty:
#获取公司代码对应的证券简称,SecuMain表中的名称中有空格,需去除
name = df1.iloc[0,1].replace(' ','')
df2 = get_all_securities()
if (df2['display_name']==name).any():
symbol = df2[df2['display_name']==name].index[0]
return symbol
调用代码转换函数,将公司代码转换为证券代码:
unbanned['CompanyCode'] = unbanned['CompanyCode'].map(com2sec)
#‘CompanyCode’一列可能包含空值,故去除所有空值所在行
unbanned = unbanned.dropna()
因为不同板块的参照基准不同,我们还想通过个股的证券代码得到它对应的基准指数,于是定义基准获取函数如下:
def ref(stock):
#深圳主板证券代码前3位是000,基准指数为深证成指399001
if stock[0:3]=='000':
reference = '399001.XSHE'
#中小板证券代码前3位是002,基准指数为中小板指399005
elif stock[0:3]=='002':
reference = '399005.XSHE'
#创业板证券代码前3位是300,基准指数为创业板指399006
elif stock[0:3]=='300':
reference = '399006.XSHE'
#上海主板证券代码首位是6,基准指数为上证综指000001
elif stock[0]=='6':
reference = '000001.XSHG'
#其它情形的比较基准设为沪深300指数000300
else:
reference = '000300.XSHG'
return reference
通过调用基准获取函数,在unbanned中新增一列‘reference’来记录个股对应的基准指数:
unbanned['reference'] = unbanned['CompanyCode'].map(ref)
再来考虑“解禁比例”的数据处理。首先绘制“Proportion1”的累计分布直方图:
plt.xlabel('Proportion(%)',fontdict={'family':'Times New Roman','size':16})
plt.ylabel('Frequency',fontdict={'family':'Times New Roman','size':16})
plt.xlim(0,200)
plt.yticks(fontsize=13)
plt.xticks(range(0,210,25),fontsize=13)
plt.grid(linestyle='--',linewidth=1,axis='y')
plt.hist(unbanned['Proportion1'],bins=40,normed=True,cumulative=True,\
align= 'mid',facecolor="yellow",edgecolor="black",alpha=0.7)
plt.show()
从上图中看出,解禁比例25%以下的数据最集中,故25%可近似认为是解禁比例的重要分水岭。因而我们以25%为分界线,将“解禁比例”划分为2个区间状态:“小于25%”和“大于25%”,分别命名为“25-”和“25+”,再将“解禁比例”的数值转换为对应的区间状态。为此,定义状态转换函数如下:
def val2sta(value):
if value<25:
status = '25-'
else:
status = '25+'
return status
调用状态转换函数将‘Proportion1’一列转化为区间状态:
unbanned['Proportion1'] = unbanned['Proportion1'].map(val2sta)
对于‘StartDateForFloating’一列的数据,删除时间部分,保留日期部分:
unbanned['StartDateForFloating']=unbanned['StartDateForFloating'].map(lambda x: str(x).split(' ')[0])
最后,为方便引用,我们对各列重新命名:
unbanned.rename(columns={'CompanyCode':'security',\
'StartDateForFloating':'date',\
'Proportion1':'proportion',\
'SourceType':'type'},\
inplace=True)
为规避次新股行情对股价走势的影响,我们过滤掉上市短于1年的股票:
#过滤次新股
for i in unbanned.index:
#获取证券代码
security = unbanned.loc[i,'security']
#获取解禁日期(字符串类型)
release_date = unbanned.loc[i,'date']
#将解禁日期先转化为日期时间类型,再转为日期类型
release_date = dt.datetime.strptime(release_date,'%Y-%m-%d').date()
#获取证券的上市日期(日期类型)
list_date = get_security_info(security).start_date
#上市不超过1年,则置证券代码为空值
if release_date-list_date<dt.timedelta(365):
unbanned.loc[i,'security']=np.nan
unbanned = unbanned.dropna()
至此,得到的数据展示如下:
unbanned.tail()
security | date | proportion | type | reference | |
---|---|---|---|---|---|
1807 | 002077.XSHE | 2019-05-31 | 25- | 24 | 399005.XSHE |
1808 | 002467.XSHE | 2019-05-31 | 25- | 80 | 399005.XSHE |
1809 | 002591.XSHE | 2019-05-31 | 25- | 24 | 399005.XSHE |
1810 | 002752.XSHE | 2019-05-31 | 25+ | 82 | 399005.XSHE |
1811 | 002800.XSHE | 2019-05-31 | 25+ | 82 | 399005.XSHE |
我们下一步是计算股票在解禁前后的绝对涨幅和相对涨幅(比较基准为板块指数)。我们的思路是:以解禁日后第10个交易日为期末日期,取前20个交易日个股和基准指数的收盘价进行处理。为获取某交易日后第10个交易日的日期,定义如下日期获取函数:
#获取以date为起始日的第n个交易日期,n默认为10
def later(date,n=10):
#将字符串型转化为日期时间型
date1= dt.datetime.strptime(date,'%Y-%m-%d')
#确定起始日期
start = date1
#确定终止日期。自然日的范围要覆盖10个交易日,此处取自然日间隔为n+20
end = date1 + dt.timedelta(n+20)
#获取连续n+10个交易日的交易日期
days = get_trade_days(start,end)
#返回第n个交易日的日期
date2 = dt.datetime.strftime(days[n-1],'%Y-%m-%d')
return date2
例如,从‘2019-05-06’开始的第10个交易日为‘2019-05-17’:
later('2019-05-06')
'2019-05-17'
我们在unbanned中新增4列:‘absolute1’、‘absolute2’、‘relative1’和‘relative2’,分别记录解禁前10日的绝对涨幅、解禁后10日的绝对涨幅、解禁前10日的相对涨幅和解禁后10日的相对涨幅。
#定义解禁日期前后的时间窗口
n = 10
#遍历unbanned所有行索引
for i in unbanned.index:
#获取证券代码
security = unbanned.loc[i,'security']
#获取基准指数
reference = unbanned.loc[i,'reference']
#获取解禁日期
date = unbanned.loc[i,'date']
#获取解禁后第10个交易日期
end = later(date)
#获取股票和指数在解禁前后10天的收盘价(共20天)
panel = get_price([security,reference],count=2*n,end_date=end,fields=['close'])
#记录股票解禁10天前的绝对涨幅
unbanned.loc[i,'absolute1'] = panel['close'][security][n-1]/panel['close'][security][0]-1
#记录股票解禁10天后的绝对涨幅
unbanned.loc[i,'absolute2'] = panel['close'][security][-1]/panel['close'][security][-n]-1
#记录指数解禁10天前的涨幅
unbanned.loc[i,'ref_ret1'] = panel['close'][reference][n-1]/panel['close'][reference][0]-1
#记录指数解禁10天后的涨幅
unbanned.loc[i,'ref_ret2'] = panel['close'][reference][-1]/panel['close'][reference][-n]-1
#循环结束
#新增一列‘relative1’记录股票解禁10天前的相对涨幅
unbanned['relative1'] = unbanned['absolute1']-unbanned['ref_ret1']
#新增一列‘relative2’记录股票解禁10天前的相对涨幅
unbanned['relative2'] = unbanned['absolute2']-unbanned['ref_ret2']
#展示最后5行数据(格式化显示为百分数,下同)
unbanned[['absolute1','absolute2','relative1','relative2','ref_ret1','ref_ret2']]\
.applymap(lambda x: format(x, '.1%')).tail()
absolute1 | absolute2 | relative1 | relative2 | ref_ret1 | ref_ret2 | |
---|---|---|---|---|---|---|
1807 | -26.0% | 11.5% | -24.9% | 12.6% | -1.2% | -1.1% |
1808 | -14.5% | 4.9% | -13.3% | 6.0% | -1.2% | -1.1% |
1809 | -5.4% | -4.8% | -4.2% | -3.7% | -1.2% | -1.1% |
1810 | -1.0% | -3.7% | 0.2% | -2.7% | -1.2% | -1.1% |
1811 | 0.0% | -5.5% | 1.2% | -4.4% | -1.2% | -1.1% |
首先来看股价在解禁前后的平均涨幅:
unbanned.groupby(['proportion','type'])[['absolute1','absolute2','relative1','relative2']]\
.mean().applymap(lambda x: format(x, '.1%')).tail()
absolute1 | absolute2 | relative1 | relative2 | ||
---|---|---|---|---|---|
proportion | type | ||||
25+ | 24 | -3.5% | -0.9% | -2.6% | -0.4% |
82 | -5.2% | -1.1% | -3.8% | -0.4% | |
25- | 24 | -2.3% | -0.1% | -1.1% | 0.5% |
80 | -2.4% | -0.7% | -0.5% | 0.5% | |
82 | -2.1% | -0.3% | -0.5% | 0.3% |
从上图中我们可以得到3个结论:1、无论是绝对涨幅还是相对涨幅,解禁后比解禁前都有显著提高。2、解禁比例25-的涨幅高于解禁比例25+的同期涨幅。3、不同解禁类型的涨幅无明显差异。
#每种情形下,解禁后绝对涨幅大于解禁前绝对涨幅的比例
condition = unbanned['absolute2']>unbanned['absolute1']
df1 = unbanned[condition].groupby(['proportion','type'])[['security']].count()
df2 = unbanned.groupby(['proportion','type'])[['security']].count()
(df1/df2).applymap(lambda x: format(x, '.1%')).tail()
security | ||
---|---|---|
proportion | type | |
25+ | 24 | 55.2% |
82 | 61.0% | |
25- | 24 | 55.7% |
80 | 55.0% | |
82 | 55.0% |
#每种情形下,解禁后相对涨幅大于解禁前相对涨幅的比例
condition = unbanned['relative2']>unbanned['relative1']
df1 = unbanned[condition].groupby(['proportion','type'])[['security']].count()
df2 = unbanned.groupby(['proportion','type'])[['security']].count()
(df1/df2).applymap(lambda x: format(x, '.1%')).tail()
security | ||
---|---|---|
proportion | type | |
25+ | 24 | 59.5% |
82 | 61.0% | |
25- | 24 | 58.7% |
80 | 55.2% | |
82 | 57.4% |
通过两类解禁比例和三种解禁类型下的分类汇总,我们还能看出:解禁后股价表现强于解禁前的概率不低于55%,利空释放效应比较明显。根据结论1不难得到推论:如果股票解禁前的涨幅大于0,则预期股票解禁后的涨幅也大于0。而根据结论2,我们可以将投资范围局限于更强势的股票,即解禁比例小于25的股票。于是,我们可以在解禁日当天买入之前涨幅大于0且解禁比例小于25的股票,持有10天后抛售。
策略1:在解禁日等金额买入解禁前10日绝对涨幅大于0,且解禁比例小于25的股票,10个交易日后平仓。
#确定数据筛选条件:解禁比例小于25且解禁前绝对涨幅大于0
conditon = (unbanned['proportion']=='25-')&(unbanned['absolute1']>0)
#计算策略1单个事件的平均年化收益率(假设一年240个交易日)
print(format(unbanned[conditon]['absolute2'].mean()*24,'.1%'))
-12.1%
结果显示,单个事件的年化平均收益为-12.1%。收益为负的结果似乎与我们之前的结论矛盾。究其原因,应该跟同期的指数表现相关。我们通过计算同期指数收益来验证这个判断:
#计算指数的同期平均年化收益率
print(format(unbanned[conditon]['ref_ret2'].mean()*24,'.1%'))
-21.7%
可以看到,指数的同期平均年化收益率为-21.7%,显著弱势于同期个股,说明个股表现受到了指数的拖累,而事件释放效应不足以抵消市场环境的影响。但是换个角度,单个事件的平均收益率高于同期指数,给了我们获取相对收益的机会,继而派生出相对收益策略。
策略2:在解禁日等金额买入解禁前10日绝对涨幅大于0,且解禁比例小于25的股票,并按对冲比例1:1做空对应指数,10个交易日后平仓。
#计算策略2单个事件的平均年化收益率(假设一年240个交易日)
print(format(unbanned[conditon]['relative2'].mean()*24,'.1%'))
9.6%
策略2的平均年化收益为9.6%。策略2是一个有显著正回报的策略,但在实际操作中其仍存在局限性。因为我们的收益计算基于4个假设:1、无交易成本;2、指数可做空;3、可实现完全对冲;4、以收盘价开平仓。假设1会使实际收益略有降低。对于假设2和假设3,现实中难以找到相关指数的完美替代品,一种选择是做空股指期货,但期指合约标的有限(只有沪深300,上证50和中证500),且期价与现价走势有差异,难以做到完全对冲,期货保证金的占用也间接降低期望收益。对于假设4,由于当前采用集合竞价机制确定收盘价,也有无法成交或不能以理论价格成交的风险。
本社区仅针对特定人员开放
查看需注册登录并通过风险意识测评
5秒后跳转登录页面...
移动端课程