Py学习  »  Python

【中金固收·技术】如何快速分辨趋势——Python实现趋势分析及债市中的适应性调整

中金固定收益研究 • 2 年前 • 489 次点击  

一、概述


对技术分析最常见的误解在于,认为“看图”就是预测。《适合转债的技术分析——体系篇》中,我们介绍我们在转债分析中,常用的利弗莫尔体系及缠论等补充技巧——但无论哪种技术手段,我们都希望明确,技术分析的核心任务在“分类”。即在回答“当下是什么样的市场”的情况下,尽力探索应对的方式。虽然在转债报告中,我们已经做过论述,但这里我们还想对利弗莫尔的趋势分析进行一个尽可能简洁的回顾:


1. 走势分类、只抓趋势: 价格走势分为上下行趋势、自然回撤和自然反弹以及次要走势。在上行趋势中做多,是多头市场中最主要的获利来源;


2. 趋势与关键点定义: 上行趋势在不断创新高,直到出现一定程度回撤(原文为“6个点”以上),计入自然回撤。其他介于二者的波动为次要波动。此前所创造的高点,以及首次自然回撤创造的低点,分别为高、低关键点。后续行情若有效突破高点,则认定趋势延续,有效跌破低点则认定行情转入下行趋势。下行趋势则相反。



图表1:示意图:趋势、自然回撤与关键点


资料来源:中金公司研究部



简单而明确,这是一个“忽略次要波动,把握大趋势”的具体化指引。但是,以下几个问题可以思考:


1. 这是一个“无参数模型”吗?—— 当然不是,这里至少“一定程度回撤”的度量,是预置的。也自然,对于不同产品(进而弹性不同),不同交易时间尺度(进而不同的信号频率容忍),以及不同风险偏好,有着不同适用参数。也因此,普通债、转债、股不应该适用同样的参数。


2. “有效突破”也是一个模糊的概念。进而会有两种理解,一种是突破一定边际程度,则认定“有效”。这里当然需要一个合适的尺度来避免晃点,同时也不至于太迟钝。另一种是结合时间,当出现一次回踩但未跌破高关键点时,认定有效突破。但何为一次完整的回踩,我们又要借助更高频的数据——最后,我们要面对分形几何问题,这超出了“简单”的范畴,也非本报告的初衷。


3. 另一个值得商榷的是,除转债之外,债券类资产的移动边界有限,已经明确形成字面意思的趋势后,是否还是好的入场点(尤其考虑到确认时滞后)。



图表2:不同“一定程度回撤”尺度模式下的趋势划分:国债

资料来源:万得资讯,中金公司研究部,注:纵轴为国债净值指数与自定义趋势划分



这里,我只解决一个问题:对于固收投资者(转债、普通债等资产),应当如何调整模型参数,以及如何看待趋势的价值。 而在此之前,我们要有一个简明的程序实现方式,来帮助我们解决问题。下面我们逐步展开。(以下,我们将判断拐点时最低考虑的回撤(反弹)称作阈值,突破关键点至少一定程度的标准,则称作边际值)



二、基本设计:一个探头,一个框架


首先,我们要设计一个“探头”,其任务是每日明确市场状态,以及关键点。 数据层面,其应当集成当前趋势类型(trend)以及高、低关键点(upperLim, lowerLim)。同时,为方便测算,我们准备了log变量,以记录过去发生过的状态。以下为初始化部分的程序实现:



图表3:债市趋势识别框架程序 (初始化部分)


class status(object):
    '''trend: 可能为up, down, upDraw, downDraw, minority
    upperLim\lowerLim: 高\低关键点
    reverseThreshold, margin分别为拐点的最低标准,以及有效突破的标准,以下称阈值与边际值
    '''

    def __init__(self, trend=None, upperLim=None, lowerLim=None,
                reverseThreshold=0.05, margin=0.02)
:

        
        self.trend, self.upperLim, self.lowerLim = trend, upperLim, lowerLim
        self.reverseThreshold, self.margin = reverseThreshold, margin
        
        self.lstKeyDates = []
        self.logger = pd.DataFrame(columns=["trend", "upperLim", "lowerLim"])
    def __str__(self):
        dictTrend = {"up":"上行趋势",
        "down":"下行趋势",
        "upDraw":"自然回撤", "downDraw":"自然反弹", "minority":"次要波动"}
        
        return  f'''当前处于{dictTrend[self.trend]}中,关键高点为{self.upperLim:.2f},关键低点为{self.lowerLim:.2f}.'''

资料来源:中金公司研究部



在“探头”接收新的价格和时间后,其将进行自我更新,根据情况进行关键点更新或趋势改判。例如,从上行趋势出发,逻辑如下图:



图表4:债市趋势识别框架程序 (基础逻辑示意)


资料来源:中金公司研究部



具体实现时,我们还需要几个辅助函数,以帮助我们将逻辑表达得更为简洁,如下:



图表5:债市趋势识别框架程序 (辅助函数部分)


def upperUpdate(self, newPoint, date):
    self.upperLim = newPoint
    self.lstKeyDates[-1] = date

def upperBreak(self, newPoint, date):
    self.upperLim = newPoint
    self.lstKeyDates.append(date)

def lowerUpdate(self, newPoint, date):
    self.lowerLim = newPoint
    self.lstKeyDates[-1] = date

def lowerBreak(self, newPoint, date):
    self.lowerLim = newPoint
    self.lstKeyDates.append(date)

资料来源:中金公司研究部



有了上述准备,以上行、自然回撤以及次要波动为例,“探头”的自更新过程如下。此处限于篇幅,我们略去下行趋势的判定(实际为上行趋势相反的操作即可):



图表6:债市趋势识别框架程序 (趋势判定部分)


def renew(self, newPoint, date):
    # 上行趋势中的判别
    if self.trend == "up":
        if newPoint > self.upperLim:
            self.upperUpdate(newPoint, date)
        elif newPoint <= self.lowerLim * (1- self.margin):
            self.trend = 'down'
            self.lowerBreak(newPoint, date)
        elif newPoint <= self.upperLim * (1 - self.reverseThreshold):
            self.trend = "upDraw"
            self.lowerBreak(newPoint, date)
    # 自然回撤中的判断
    elif self.trend == "upDraw":
        if newPoint <= self.lowerLim: 
            self.lowerUpdate(newPoint, date)
        elif newPoint >= self.upperLim* (1 + self.margin):
            self.trend = "up"
            self.upperBreak(newPoint, date)
        elif newPoint >= self.lowerLim* ( 1 + self.reverseThreshold):
            self.trend = "minority"            
            self.lstKeyDates.append(date)
    # 次要走势
    elif self.trend == "minority":
        if newPoint >= self.upperLim * (1 + self.margin):
            self.trend = "up"
            self.upperBreak(newPoint, date)
        elif newPoint <= self.lowerLim * (1 - self.margin):
            self.trend = "down"
            self.lowerBreak(newPoint, date)
    self.logger.loc[date] = [self.trend, self.upperLim, self.lowerLim]

资料来源:中金公司研究部



至此,“探头”变量设计完成。而测算流程无非是让探头从头到尾读取一遍时间序列数据,因此整体框架反而更加简单。这里除了常规初始化外,我们还要额外定义一个“寻找起点”的小函数——因为价格序列在一开始是没有方向的,我们根据其累积出的变化值,当其达到某个阈值(例如2%)时,认定起点趋势。



图表7:债市趋势识别框架程序 (寻找起点部分)


class LivermoreAnalysis(object):
    def __init__(self, data):
        '''data必为pd.Series格式
        self.status为状态单元
        '
''
        self.data = data
        self.status = status()
        
    def initSeries(self, thres=0.02):
        
        srs = self.data.copy()
        srs = srsFillContinousUpAndDown(srs)
        _srs01 = ((srs.pct_change() + 1.0).cumprod() - 1.0).apply(lambda x: x if abs(x) >= thres else np.nan)
        
        initIndex = _srs01.first_valid_index()
        self.status.lstKeyDates = [srs.index[0], initIndex]
        
        self.status.trend = "up" if _srs01[initIndex] > 0 else "down"
        if self.status.trend == "up":
            self.status.upperLim, self.status.lowerLim = srs[initIndex], srs[0]
        else:
            self.status.upperLim, self.status.lowerLim = srs[0], srs[initIndex]
        
        return srs, initIndex

资料来源:中金公司研究部



这里,我们还用到了一个srsFillContinousUpAndDown函数,该函数是为了降低计算负荷,因而将连续涨跌都做合并处理。但对于处理速度没有要求的投资者,并不必要。



图表8:债市趋势识别框架程序 (辅助函数部分2)


class LivermoreAnalysis(object):
    def __init__(self, data):
        '''data必为pd.Series格式
        self.status为状态单元
        '
''
        self.data = data
        self.status = status()
        
    def initSeries(self, thres=0.02):
        
        srs = self.data.copy()
        srs = srsFillContinousUpAndDown(srs)
        _srs01 = ((srs.pct_change() + 1.0).cumprod() - 1.0).apply(lambda x: x if abs(x) >= thres else np.nan)
        
        initIndex = _srs01.first_valid_index()
        self.status.lstKeyDates = [srs.index[0], initIndex]
        
        self.status.trend = "up" if _srs01[initIndex] > 0 else "down"
        if self.status.trend == "up":
            self.status.upperLim, self.status.lowerLim = srs[initIndex], srs[0]
        else:
            self.status.upperLim, self.status.lowerLim = srs[0], srs[initIndex]
        
        return srs, initIndex

资料来源:中金公司研究部



而最后需要用到的,便是让“探头”完整走过价格序列,处理非常简单,此处不赘述。



图表9:债市趋势识别框架程序 (分析输出)


def srsAnalysis(self, initThres, reverseThreshold, margin, fig=True):

    srs, initIndex = self.initSeries(initThres)
    self.status.reverseThreshold, self.status.margin = reverseThreshold, margin
    start = srs.index[srs.index.get_loc(initIndex) + 1]
    for date in srs[start:].index:
        newPoint = srs[date]
        self.status.renew(newPoint, date)
            
    if fig:
        srs2plot = srsFillContinousUpAndDown(srs[self.status.lstKeyDates]).plot(figsize=(15,10))
    return srs[self.status.lstKeyDates], self.status

资料来源:中金公司研究部



示例:假设srs为某债券价格走势,我们定义50bps以上考虑反转,突破关键点20bps以上认定有效突破,那么只需要进行如下操作即可。



图表10:债市趋势识别框架程序 (示例)


lv = LivermoreAnalysis(srs)
lv .srsAnalysis(0.02, 0.005, 0.002, fig=True)

资料来源:中金公司研究部



三、探索:各类债券资产,适合趋势投资吗?


1. 利率债:大级别“一致预期”大概率必惩,小阈值顺势可取。由于国债期货的存在,利率债相对很容易做出比较优美的趋势线 —— 但是,事后的叙述,与事前、事中的逐步判定,存在了较大差别。而债券市场本身弱于股票的波动空间,也让较大级别的趋势认定,存在了高昂的成本。加上债券投资者相对一致的交易行为,让我们首先看到的是:大阈值下,趋势形成并经一致确认后,大概率都临近终结——这甚至是比较稳健的反向指标。下图为在0.8%阈值,0.4%边际值下,认定上行趋势(不含自然回撤及附带的次级波动阶段,下同)做空、下行趋势做多的净值走势:



图表11:利率债0.8%阈值,0.4%边际值下,反向趋势交易示意

资料来源:万得资讯,中金公司研究部,策略起始日2017年12月29日,策略起始净值为1.0



但当我们把阈值放小,情况逐渐也在变化,并在某个水平趋于稳定。例如我们忽略0.2%以内的波动时,在上行趋势、上行趋势后的自然回撤中保持做多,下行趋势、下行趋势后的自然反弹中做空——即顺势而为,效果同样稳定。



图表12:利率债0.2%阈值,顺势趋势交易示意

资料来源:万得资讯,中金公司研究部,策略起始日2017年12月29日,策略起始净值为1.0



不难理解,如果以上二者结合,即忽略小波动(0.2%以内),顺势交易,但在大级别趋势(0.8%以上)得到确认后反向交易,亦能得到更好的结果。



图表13:利率债0.2%阈值,顺势趋势,0.8%趋势确认反向交易示意

资料来源:万得资讯,中金公司研究部,策略起始日2017年12月29日,策略起始净值为1.0



2. 信用债:适用较小的阈值,疑似存在比利率更强的周期性。我们未进行个券方面的尝试,但在指数层面,我们尝试了各有基金以其为基准指数的“沪质城投”和“中高企债”,均只用净价指数。显然这些指数的日波动都要明显小于国债期货,与转债更无可比性,因而在较大阈值下,大概率无法做到有价值的行情切割。我们将阈值同样设到0.2%,基本可以描绘一年一种,比较明显的几波行情:



图表14:沪质城投的趋势切割(阈值0.2%,边际值0.05%)

资料来源:万得资讯,中金公司研究部,注:纵轴为沪质城投(H01018.CSI)曲线及趋势拟合



同样,利用这类切割,进行顺势交易,效果尚可。但是,对于这类指数来说,似乎更有意义的操作模式,是在自然回撤时建仓买入,趋势确认后卖出(自然反弹时则相反)。



图表15:自然回撤迈入,趋势确认后卖出示意图

资料来源:万得资讯,中金公司研究部,策略起始日2017年12月29日,策略起始净值为1.0



但为什么信用债类的指数,都不适用于更大阈值的趋势切割呢?一方面在于从结果上看,上述指数都更有周期性——不一定是固定的周期,但显然如果其近些年的走势是一个函数f(x),其更适合傅里叶展开,而非泰勒。另一方面,由于缺少交易,其用于确认趋势的折返较少,对于机器而言,相当于缺少计算资源。经过一些简单试验,也不难发现相比于这里的趋势切割,均线分析都会更加适用。我们也不在这里,进一步地对于较大阈值(例如0.8%以上)的趋势切割进行展开。


3. 可转债:适合趋势交易,但与适合适当逆势不矛盾。我们在转债市场已经进行了很多技术、趋势分析方面的尝试,无论是诸多量化策略,还是我们定期发布的十大个券,都基本证明的量价对于转债研究的核心价值。当然对于转债指数而言,由于其衍生品属性以及编制方式的特殊性,一定程度上也会削弱趋势的价值。一个值得参考的结果是:在忽略1%波动的情况下,顺势而为有长期获利能力。但如果下行趋势明显到绝大多数人都可以察觉,例如设定2.5%以上的阈值时,仍可确认的下行趋势,此时反向抄底可以考虑。二者结合可以发现转债指数多数有价值的买点,效果如下:



图表16:转债趋势交易示意图

资料来源:万得资讯,中金公司研究部,策略起始日2017年12月29日,策略起始净值为1.0



小结


1. 设定20bps左右的阈值(20bps指价格波动,而非收益率),可以将多数纯债券类的趋势予以刻画。投资者可以较为方便地了解,当下的环境处于哪一阶段,这也是我们认为,技术分析最基础的任务;


2. 在机器学习、深度学习大范围普及,GPU广泛应用于市场的2022年,我们并不希望强行证明利弗莫尔在上个世纪30年代提出的交易依然多么有效,尽管其在一定范围内仍能起到提示交易的作用。但其趋势交易的思想依然提供了一个良好的框架,与后来的技术相容。


3. 实际上,技术分析后来的发展本身也是在不断地弥补这一体系的不足,例如:

1)如何尽量降低趋势转换时,确认的成本;

2)有没有可能在左侧发现拐点,例如所谓“背驰”;

3)后来人们也发现,居于上、下行趋势之间的震荡状态,也是更低级别的趋势。


4. 以上结果我们列于下表:



图表17:趋势交易策略小结

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



文章来源

本文摘自:2022年2月11日已经发布的《如何快速分辨趋势——Python实现趋势分析及债市中的适应性调整


杨 冰 SAC执业证书编号:S0080515120002;SFC CE Ref: BOM868

房 铎 SAC执业证书编号:S0080519110001

罗 凡 SAC执业证书编号:S0080120070107

陈健恒 SAC执业证书编号:S0080511030011;SFC CE Ref: BBM220




法律声明

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


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