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

一、概述


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


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
 
508 次点击