
对技术分析最常见的误解在于,认为“看图”就是预测。在《适合转债的技术分析——体系篇》中,我们介绍我们在转债分析中,常用的利弗莫尔体系及缠论等补充技巧——但无论哪种技术手段,我们都希望明确,技术分析的核心任务在“分类”。即在回答“当下是什么样的市场”的情况下,尽力探索应对的方式。虽然在转债报告中,我们已经做过论述,但这里我们还想对利弗莫尔的趋势分析进行一个尽可能简洁的回顾:
1. 走势分类、只抓趋势: 价格走势分为上下行趋势、自然回撤和自然反弹以及次要走势。在上行趋势中做多,是多头市场中最主要的获利来源;
2. 趋势与关键点定义: 上行趋势在不断创新高,直到出现一定程度回撤(原文为“6个点”以上),计入自然回撤。其他介于二者的波动为次要波动。此前所创造的高点,以及首次自然回撤创造的低点,分别为高、低关键点。后续行情若有效突破高点,则认定趋势延续,有效跌破低点则认定行情转入下行趋势。下行趋势则相反。
简单而明确,这是一个“忽略次要波动,把握大趋势”的具体化指引。但是,以下几个问题可以思考:
1. 这是一个“无参数模型”吗?—— 当然不是,这里至少“一定程度回撤”的度量,是预置的。也自然,对于不同产品(进而弹性不同),不同交易时间尺度(进而不同的信号频率容忍),以及不同风险偏好,有着不同适用参数。也因此,普通债、转债、股不应该适用同样的参数。
2. “有效突破”也是一个模糊的概念。进而会有两种理解,一种是突破一定边际程度,则认定“有效”。这里当然需要一个合适的尺度来避免晃点,同时也不至于太迟钝。另一种是结合时间,当出现一次回踩但未跌破高关键点时,认定有效突破。但何为一次完整的回踩,我们又要借助更高频的数据——最后,我们要面对分形几何问题,这超出了“简单”的范畴,也非本报告的初衷。
3. 另一个值得商榷的是,除转债之外,债券类资产的移动边界有限,已经明确形成字面意思的趋势后,是否还是好的入场点(尤其考虑到确认时滞后)。
图表2:不同“一定程度回撤”尺度模式下的趋势划分:国债
资料来源:万得资讯,中金公司研究部,注:纵轴为国债净值指数与自定义趋势划分
这里,我只解决一个问题:对于固收投资者(转债、普通债等资产),应当如何调整模型参数,以及如何看待趋势的价值。 而在此之前,我们要有一个简明的程序实现方式,来帮助我们解决问题。下面我们逐步展开。(以下,我们将判断拐点时最低考虑的回撤(反弹)称作阈值,突破关键点至少一定程度的标准,则称作边际值)
首先,我们要设计一个“探头”,其任务是每日明确市场状态,以及关键点。 数据层面,其应当集成当前趋势类型(trend)以及高、低关键点(upperLim, lowerLim)。同时,为方便测算,我们准备了log变量,以记录过去发生过的状态。以下为初始化部分的程序实现:
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}.'''
在“探头”接收新的价格和时间后,其将进行自我更新,根据情况进行关键点更新或趋势改判。例如,从上行趋势出发,逻辑如下图:
具体实现时,我们还需要几个辅助函数,以帮助我们将逻辑表达得更为简洁,如下:
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)
有了上述准备,以上行、自然回撤以及次要波动为例,“探头”的自更新过程如下。此处限于篇幅,我们略去下行趋势的判定(实际为上行趋势相反的操作即可):
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%)时,认定起点趋势。
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函数,该函数是为了降低计算负荷,因而将连续涨跌都做合并处理。但对于处理速度没有要求的投资者,并不必要。
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
而最后需要用到的,便是让“探头”完整走过价格序列,处理非常简单,此处不赘述。
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以上认定有效突破,那么只需要进行如下操作即可。
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%以内的波动时,在上行趋势、上行趋势后的自然回撤中保持做多,下行趋势、下行趋势后的自然反弹中做空——即顺势而为,效果同样稳定。
资料来源:万得资讯,中金公司研究部,策略起始日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)曲线及趋势拟合
同样,利用这类切割,进行顺势交易,效果尚可。但是,对于这类指数来说,似乎更有意义的操作模式,是在自然回撤时建仓买入,趋势确认后卖出(自然反弹时则相反)。
资料来源:万得资讯,中金公司研究部,策略起始日2017年12月29日,策略起始净值为1.0
但为什么信用债类的指数,都不适用于更大阈值的趋势切割呢?一方面在于从结果上看,上述指数都更有周期性——不一定是固定的周期,但显然如果其近些年的走势是一个函数f(x),其更适合傅里叶展开,而非泰勒。另一方面,由于缺少交易,其用于确认趋势的折返较少,对于机器而言,相当于缺少计算资源。经过一些简单试验,也不难发现相比于这里的趋势切割,均线分析都会更加适用。我们也不在这里,进一步地对于较大阈值(例如0.8%以上)的趋势切割进行展开。
3. 可转债:适合趋势交易,但与适合适当逆势不矛盾。我们在转债市场已经进行了很多技术、趋势分析方面的尝试,无论是诸多量化策略,还是我们定期发布的十大个券,都基本证明的量价对于转债研究的核心价值。当然对于转债指数而言,由于其衍生品属性以及编制方式的特殊性,一定程度上也会削弱趋势的价值。一个值得参考的结果是:在忽略1%波动的情况下,顺势而为有长期获利能力。但如果下行趋势明显到绝大多数人都可以察觉,例如设定2.5%以上的阈值时,仍可确认的下行趋势,此时反向抄底可以考虑。二者结合可以发现转债指数多数有价值的买点,效果如下:
资料来源:万得资讯,中金公司研究部,策略起始日2017年12月29日,策略起始净值为1.0
1. 设定20bps左右的阈值(20bps指价格波动,而非收益率),可以将多数纯债券类的趋势予以刻画。投资者可以较为方便地了解,当下的环境处于哪一阶段,这也是我们认为,技术分析最基础的任务;
2. 在机器学习、深度学习大范围普及,GPU广泛应用于市场的2022年,我们并不希望强行证明利弗莫尔在上个世纪30年代提出的交易依然多么有效,尽管其在一定范围内仍能起到提示交易的作用。但其趋势交易的思想依然提供了一个良好的框架,与后来的技术相容。
3. 实际上,技术分析后来的发展本身也是在不断地弥补这一体系的不足,例如:
1)如何尽量降低趋势转换时,确认的成本;
2)有没有可能在左侧发现拐点,例如所谓“背驰”;
3)后来人们也发现,居于上、下行趋势之间的震荡状态,也是更低级别的趋势。
4. 以上结果我们列于下表: