对技术分析最常见的误解在于,认为“看图”就是预测。
在 《适合转债的技术分析——体系篇》 中,我们介绍我们在转债分析中,常用的利弗莫尔体系及缠论等补充技巧——但无论哪种技术手段,我们都希望明确,技术分析的核心任务在“分类”。即在回答“当下是什么样的市场”的情况下,尽力探索应对的方式。虽然在转债报告中,我们已经做过论述,但这里我们还想对利弗莫尔的趋势分析进行一个尽可能简洁的回顾:
1. 走势分类、只抓趋势:
价格走势分为上下行趋势、自然回撤和自然反弹以及次要走势。在上行趋势中做多,是多头市场中最主要的获利来源;
2. 趋势与关键点定义:
上行趋势在不断创新高,直到出现一定程度回撤(原文为“6个点”以上),计入自然回撤。其他介于二者的波动为次要波动。此前所创造的高点,以及首次自然回撤创造的低点,分别为高、低关键点。后续行情若有效突破高点,则认定趋势延续,有效跌破低点则认定行情转入下行趋势。下行趋势则相反。
图表1:示意图:趋势、自然回撤与关键点
资料来源:中金公司研究部
简单而明确,这是一个“忽略次要波动,把握大趋势”的具体化指引。但是,以下几个问题可以思考:
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:
.2
f}
,关键低点为
{self.lowerLim:
.2
f}
.'''
资料来源:中金公司研究部
在“探头”接收新的价格和时间后,其将进行自我更新,根据情况进行关键点更新或趋势改判。例如,从上行趋势出发,逻辑如下图:
资料来源:中金公司研究部
具体实现时,我们还需要几个辅助函数,以帮助我们将逻辑表达得更为简洁,如下:
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%以内的波动时,在上行趋势、上行趋势后的自然回撤中保持做多,下行趋势、下行趋势后的自然反弹中做空——即顺势而为,效果同样稳定。
图表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实现趋势分析及债市中的适应性调整
》
向上滑动参见完整法律声明及二维码
返回搜狐,查看更多
责任编辑:
平台声明:该文观点仅代表作者本人,搜狐号系信息发布平台,搜狐仅提供信息存储空间服务。