资料来源:万得资讯,中金公司研究部
注:图来自2020年8月28日发布的《有关转债的技术分析——体系篇》
简单来说,做出这样的图,我们需要:
1、处理K线之间的包含关系,即若出现前后2个K线,其中一个被另一个完全包含的情况,我们需要合并处理;
2、划分顶分型和底分型,定义见上图;
3、连接“合格”的顶和底,组成图上的笔。
下面我们来逐步分解:首先处理K线之间的包含关系
。实践中我们发现此举主要是为了避免某一个交易日,即是顶也是底的状态。其中可能出现两种可能性,即前后2个K线,后者包含前者,以及前者包含后者。我们最终需要将存在包含关系的K线图合并,其中原本处于上行方向的,合并后的新K线的最高、最低价均为原本两个K线图的最高者,反之则相反。
K线的合并——以光大转债正股为例
资料来源:万得资讯,中金公司研究部
这里不需要更高级的算法处理,除了应用Python语言原生的Interval库(主要用来处理区间),只是要借助几个函数来分步处理:
1)处理确认两个K线是否有包含关系,以及这种关系的类型,这里要用到下面的isIncluding和intervalCompute两个辅助函数;
2)按照此前走势方向,合并这两个K线图,即includingProcess和_reviseInclude。程序实现逻辑如下:
处理K线之间包含关系的程序实现(一)
def
intervalCompute
(intvA, intvB)
:
# 计算A、B区间是否重叠,如有,则返回重叠部分,否则返回方向关系(b > a则为up反之down),和None
if
intvA.overlaps(intvB):
intvRet = Interval(max((intvA.lower_bound, intvB.lower_bound)), min((intvA.upper_bound, intvB.upper_bound)))
return
"overlap"
, intvRet
elif
intvA.upper_bound < intvB.lower_bound:
return
"up"
,
None
else
:
return
"down"
,
None
def
isIncluding
(intvA, intvB)
:
# 计算AB是否为包含关系,只要一方包含另一方,则返回True, 否则False
isOverlap, intvOvlp = intervalCompute(intvA, intvB)
if
isOverlap ==
"overlap"
and
intvOvlp == intvA:
# 重复段为前者,即后包前
return
True
,
0
if
isOverlap ==
"overlap"
and
intvOvlp == intvB:
# 重复段为后者,即前包后
return
True
,
1
else
:
# 无包含关系
return
False
,
None
def
includingProcess
(intvA, intvB, direction=
"up"
)
:
# 计算相互有包含关系的A、B合并后的新区间,拟缠论规则
if
direction ==
"up"
:
return
Interval(max((intvA.lower_bound, intvB.lower_bound)), max((intvA.upper_bound, intvB.upper_bound)))
elif
direction ==
"down"
:
return
Interval(min((intvA.lower_bound, intvB.lower_bound)), min((intvA.upper_bound, intvB.upper_bound)))
def
_reviseInclude
(dfKlinesCopy, intvRet, close, iloc)
:
# 辅助函数,用于最后按包含关系修改dfKlinesCopy
dfKlinesCopy[
"HIGH"
][iloc] = intvRet.upper_bound
dfKlinesCopy[
"LOW"
][iloc] = intvRet.lower_bound
dfKlinesCopy[
"CLOSE"
][iloc] = close
return
dfKlinesCopy
资料来源:万得资讯,中金公司研究部
有了这些准备工作,处理包含关系并合并的工作就能相对简单完成。我们用_exclude函数作为这一步的汇总处理,程序逻辑如下:
处理K线之间包含关系的程序实现(二)
def
_exInclude
(dfKlines)
:
# 私有函数,处理包含关系,lastValid和lstValid用于在后面的遍历中标识有意义的K线的记录
# 同时为避免破坏数据源,先制作了一个备份变量dfKlinesCopy
lastValid =
0
; lstValid = [dfKlines.index[
0
]]
dfKlinesCopy = dfKlines.copy()
for
i, idx
in
enumerate(dfKlines.index):
# 把当日股价运行区间变换成一个区间对象,以便用上面的“intervalCompute”
intvKi = Interval(dfKlines[
"LOW"
][i], dfKlines[
"HIGH"
][i])
intvLast = Interval(dfKlinesCopy[
"LOW"
][lastValid], dfKlinesCopy[
"HIGH"
][lastValid])
if
i >
1
:
biIn, inType = isIncluding(intvLast, intvKi)
if
not
biIn:
# 如果没有包含关系,不改动数据
lastValid = i
lstValid.append(idx)
else
:
direction =
"up"
if
dfKlines[
"HIGH"
][lastValid] >= dfKlines[
"HIGH"
][lastValid
-1
]
else
"down"
intvRet = includingProcess(intvLast, intvKi, direction)
if
inType ==
1
:
dfKlinesCopy = _reviseInclude(dfKlinesCopy, intvRet, dfKlines[
"CLOSE"
][i], lastValid)
elif
inType ==
0
:
dfKlinesCopy = _reviseInclude(dfKlinesCopy, intvRet, dfKlines[
"CLOSE"
][i], i)
lastValid = i
lstValid.pop()
lstValid.append(idx)
# 最后,我们只输出在lstValid中的数据
dfRet = dfKlinesCopy.loc[lstValid]
return
dfRet
资料来源:万得资讯,中金公司研究部
下一步较为关键,我们需要根据K线的高点、低点来生成“分型”。
实际上,技术上分型的叫法常与几何中的“分形”混淆,而我们认为其更不容易出现误解的概念应该是“拐点”。技术上的经典定义为某K线图的高点高于其两侧的高点则定义为顶点(也有相应底点的定义) —— 但这样会形成较多互相临近的“假拐点”,于是我们选择用《混沌交易法》中的方式,需要某K线高于前后各两个K线图的高点,才定义为顶点。示意如下:
分型——以光大转债正股为例
资料来源:万得资讯,中金公司研究部
而这一步只需要巧用滚动窗口的计算,即可将符合条件的点选择出来。随后,我们可以用一个DataFrame结构保存这个结果,见getRet。程序逻辑很简易,如下:
处理K线分型的程序实现
def getInflection(dfK):
srsMax, srsMin = dfK[
"HIGH"
].rolling(
5
).
max
().shift(-
2
), dfK[
"LOW"
].rolling(
5
).
min
().shift(-
2
)
lstUp =
list
(dfK.
loc
[dfK[
"HIGH"
] == srsMax].
index
)
lstDown =
list
(dfK.
loc
[dfK[
"LOW"
] == srsMin].
index
)
return
lstUp, lstDown
def getRet(dfK, lstUp, lstDown):
dfRet = pd.DataFrame(
index
=sorted(lstUp + lstDown), columns=[
"ALL"
,
"pointType"
])
dfRet.
loc
[lstUp,
"ALL"
] = dfK.
loc
[lstUp,
"HIGH"
]
dfRet.
loc
[lstUp,
"pointType"
] =
1
dfRet.
loc
[lstDown,
"ALL"
] = dfK.
loc
[lstDown,
"LOW"
]
dfRet.
loc
[lstDown,
"pointType"
] = -
1
print
dfRet
return
dfRet
资料来源:万得资讯,中金公司研究部
接下来,理论上连接顶和底就可以得到我们需要的趋势图了,但这里我们还要针对两种情况进一步提炼:
1、不能出现两个顶点相邻或者两个底点相邻的情况。由于存在多顶点相邻的情景,同时最终我们要对多个顶部相邻的情景抽取最高者(底点则相反),这里我们不得已要对顶点、底点序列进行遍历。算法逻辑如下:
避免多顶/底点相邻的程序实现
def
dropSameDirection
(dfRet)
:
dictSeg = []; flag =
0
for
i,d
in
enumerate(dfRet.index):
if
i >=
1
:
if
dfRet.pointType[i] == dfRet.pointType[i
-1
]:
if
flag ==
0
:
tempList = [dfRet.index[i
-1
], d]
flag =
1
else
:
tempList.append(d)
elif
flag ==
1
:
dictSeg.append(tempList)
flag =
0
continue
if
d == dfRet.index[
-1
]
and
flag ==
1
:
dictSeg.append(tempList)
if
len(dictSeg):
lst2drop = []
for
lst
in
dictSeg:
if
dfRet.pointType[lst[
0
]] ==
1
:
lst.remove(pd.to_numeric(dfRet.loc[lst,
"ALL"
]).argmax())
else
:
lst.remove(pd.to_numeric(dfRet.loc[lst,
"ALL"
]).argmin())
lst2drop += lst
dfRet.drop(index = lst2drop, inplace=
True
)
资料来源:万得资讯,中金公司研究部
2、为屏蔽短期变动带来的干扰,顶与底之间的时间距离不能太短。不过,有些顶和底之间的距离虽然很近,但波动足够大,尤其近两年来市场可能还会受到隔夜海外市场、大宗商品的影响,这些短线波动是有意义的。因此,我们会剔除的是:
1)底点后很快迎来顶点,且顶点相比此前一个顶点更低的情况(即虽然有底部反转的迹象,但下一个顶点很快到来,且这个底点结构没能给市场带来足够高度的反弹)。
2)同样的情况,适用于顶点。
上述算法的程序逻辑如下。这里需要注意,由于不能破坏顶、底相连的规则,我们每次要剔除偶数个点,这里借助一个flag个变量:
控制顶/底点相连的程序实现
def
dropNearPunc
(dfRet, dfK)
:
print
dfRet
dfRet[
"locInK"
] = [dfK.index.get_loc(d)
for
d
in
dfRet.index]
lst2BeDropped = []; flag =
0
for
i, d
in
enumerate(dfRet.index):
if
flag :
flag =
0
continue
if
i >
1
and
d != dfRet.index[
-1
]
and
(dfRet.locInK[i+
1
] - dfRet.locInK[i] <=
3
):
if
dfRet.pointType[i] ==
1
:
if
dfRet.ALL[i+
1
] >= dfRet.ALL[i
-1
]:
lst2BeDropped.append(d)
lst2BeDropped.append(dfRet.index[i+
1
])
flag =
1
else
:
if
dfRet.ALL[i+
1
] <= dfRet.ALL[i
-1
]:
lst2BeDropped.append(d)
lst2BeDropped.append(dfRet.index[i+
1
])
flag =
1
lstValid = sorted(set(dfRet.index).difference(lst2BeDropped))
return
dfRet.loc[lstValid]
资料来源:万得资讯,中金公司研究部
有了上面的准备,最终投入使用的函数就显得相对容易,只需要将上述辅助工具组合到一起即可。如下:
合并函数的程序实现
def
generatePunc
(dfKlines)
:
# 处理包含关系
dfK = _exInclude(dfKlines)
# 通过滚动算法,找到符合顶分型、底分型的K线,存在lstUp和lstDown里面
lstUp, lstDown = getInflection(dfK)
print
sorted(lstUp + lstDown)
# 将这些代表“拐点”的分型存入dfRet中,准备进一步过滤
dfRet = getRet(dfK, lstUp, lstDown)
# 去除同向相连的情况
dropSameDirection(dfRet)
# 去除二点过近且波动意义不大的情况
dfRet = dropNearPunc(dfRet, dfK)
# 将终结点与最后一个拐点连接
if
not
dfKlines.index[
-1
]
in
dfRet.index:
dr =
"up"
if
dfRet[
"pointType"
][
-1
] ==
-1
else
"down"
dfRet.loc[dfKlines.index[
-1
],
"ALL"
] = dfKlines.loc[dfKlines.index[
-1
],
"HIGH"
]
if
dr ==
"up"
else
dfKlines.loc[dfKlines.index[
-1
],
"LOW"
]
return
dfRet
资料来源:万得资讯,中金公司研究部
对当前转债市场,一些有用的结论:
1、哪些比较大的品种,是处于日线级别的上行中?
大中盘转债中,至少苏银、光大、东财、大秦、国君、本钢、盛虹、青农、中化EB、本钢以及海亮符合这样的要求。当然,这样的择券倾向,和我们上期周报中提到的策略(乃至我们的十大个券),也比较接近。但相比于长期、中期动量,趋势划分的结果更具灵活性。
2、按照上述每一笔连线的长度,哪些正股是转债标的中,比较富有弹性的?
当下转债的标的很多,我们只能列举一部分。不过,既然在考虑弹性,那么在择券时,一般也与价格、溢价率搭配考虑,如此方可取得不错的组合盈亏比。
正股弹性较好的转债列表(截至2021年6月25日)
资料来源:万得资讯,中金公司研究部
转债一级市场跟踪
本周新公告了2个转债预案,分别为阿拉丁(4.01亿元)与禾丰股份(15亿元)。10个转债预案通过股东大会,包括慧云钛业(4.9亿元)、东杰智能(6亿元)、会通股份(8.5亿元)、中富通(5.05亿元)、华友钴业(76亿元)、科伦药业(30亿元)、中环环保(8.64亿元)、中天精装(6.07亿元)、五洲特纸(6.7亿元)、精工钢构(20亿元);6个预案获受理,分别为立华股份(21亿元)、温州宏丰(3.21亿元)、珀莱雅(8.04亿元)、科蓝软件(5.38亿元)、华翔股份(8亿元)、百润股份(12.8亿元);闻泰科技(86亿元)与川恒股份(11.6亿元)过发审委;台华新材(6亿元)与中大力德(2.7亿元)获核准。目前已核准待发行个券合计25只,金额共计382.86亿元;已过会未核准个券10只,合计金额212.61亿元。