社区所有版块导航
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 年前 • 1082 次点击  


转债市场策略展望


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



光大转债正股日线

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

注:图来自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/116594
 
1082 次点击