Py学习  »  Python

中金固收 | 可转债:走势切割与Python实践

中金点睛 • 3 年前 • 799 次点击  


转债市场策略展望


上周周报(《离开低点两个月后》)有这样一张图受到不少投资者关注:这张图原本用来说明关于银行转债的走势,但中间的“直线”部分,是如何确定的?多数投资者可能已经猜想到这并非手动的、主观的划分,而是一种可以批量复制的程序实现。我们在日常分析策略及个券时也确实会用到这一技术,在此,我们进行简单的讨论。


光大转债正股日线

资料来源:万得资讯,中金公司研究部,

注:图来自2021年6月25日发布的《离开低点两个月后》


对转债投资者来说,将原始的走势进行此类划分的意义是什么?——主要在两方面,一是将富含“噪声”的行情波动,用更为简洁的趋势概括出来,从而进行行情分类。至少,我们在划分好这些后,我们能够知道,当前转债是走在上行趋势还是下行趋势中,以及结合MACD等,还可知道其运行阶段。对于标的普遍波动较大的转债而言,这一点尤其重要。另一方面,则在于转债投资者应当比股票投资者更关注弹性,而用每日波动计算出的弹性存在诸多弊端,比如无法反映趋势型。而如果将行情分好段,直观上平均线段的长度,就是对正股弹性一个很好的度量方式。


近期在十大推荐转债中的双环转债正股走势

资料来源:万得资讯,中金公司研究部


在此前报告《有关转债的技术分析——体系篇》中,我们曾详细介绍过利弗莫尔体系。简而言之,趋势投资就是要先对行情进行“提纯”,忽略不必要的波动,进而确定技术性趋势。而后来在此基础上,再利用很多其他理论、体系对一些细节处理,进行了改良。而我们同样在该报告中介绍的缠论,至少在行情的分类上,有了更好、更容易程序化的发展。在上面的图中,我们所展示的就是日线图上的“笔”(我们在其基础上也稍作一些修改),也即一顶和一底相连的部分。


缠论中分型、笔、线段、级别、中枢、趋势和盘整等概念

资料来源:万得资讯,中金公司研究部

注:图来自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亿元。



推荐阅读


1. 2021年7月十大转债

2. “低估”策略优化与Python实现

3. 转债退市风险测算与Python实现

4. 转债因子模型与简易Python框架

5. 新券定位:方法、边际变化与python实现


文章来源

本文摘自:2021年7月2日已经发布的《适合转债的走势划分、弹性重塑与Python实践

冰 SAC执业证书编号:S0080515120002SFC CE Ref: BOM868

铎 SAC执业证书编号:S0080519110001

罗凡 SAC执业证书编号:S0080120070107

陈健恒 SAC执业证书编号:S0080511030011SFC CE Ref: BBM220



法律声明

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

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