关于寻找顶和底的一些研究
scipy当中有一些信号处理函数,可以用来发现顶和底,比如argrelextrema。
比函数名上看,这是一个寻找相对极值点坐标的函数。我们来看一下它的用法:
def bs_signals(bars):
from scipy.signal import argrelextrema
ma = moving_average(bars["close"], 5)
peak_indexes = argrelextrema(ma, np.greater)
peaks = peak_indexes[0]
# Find valleys(min).
valley_indexes = argrelextrema(ma, np.less)
valleys = valley_indexes[0]
assert abs(len(peaks) - len(valleys)) <= 1
bars = bars[4:]
# Plot main graph.
(fig, ax) = plt.subplots()
ax.plot(np.arange(len(bars)), bars["close"], color='c')
ax.plot(np.arange(len(bars)), ma, color='b')
# Plot peaks.
peak_x = peaks
peak_y = bars['close'][peak_x]
ax.plot(peak_x, peak_y, 'gv', label="Peaks")
# Plot valleys.
valley_x = valleys
valley_y = bars['close'][valley_x]
ax.plot(valley_x, valley_y, 'r^', label="Valleys")
trades = []
gains = 1
order = None
vertex = sorted([*peaks, *valleys])
for x in vertex:
buy = x in valleys
sell = x in peaks
if buy and order is None:
order = {
"buy": bars["close"][x],
"buy_at": bars["frame"][x]
}
elif sell and order:
buy = order["buy"]
sell = bars["close"][x]
gain = sell / buy
order.update({
"sell": sell,
"sell_at": bars["frame"][x],
"gain": gain
})
gains *= gain
trades.append(order)
order = None
return gains - 1, trades
上面的函数使用argrelextrema来查找均线的顶和底。这里使用均线,是因为均线相对平滑,去除了一些噪音。这里的moving_average定义为:
import numpy as np
def moving_average(ts, win):
return np.convolve(ts, np.ones(win) / win, mode="valid")
它使用了卷积来求简单移动平均值。numpy的卷积运算速度很快,但如果想要更快的速度,可以使用bottleneck的move_mean,最高可以快6000多倍(取自官方文档,不一定是和np.convolve方法比较)。
通过给argrelextrem传入不同的比较函数(例子中是np.less和np.greater),我们可以分别查找极大值和极小值,记为peaks和valleys。
然后我们把均线、收盘价线和极值点画在同一个图上。最后,我们假设在低点买入,在高点卖出,就可以计算出策略的收益。
下图是我们以东方电气2021年11月2日为止点,对60个交易日内进行信号发现的结果:
可以看出,仅仅使用默认参数,argrelextrem就能较好地发现顶和底。我们得到的最终收益是35%,而同期该股实际上是不涨不跌。
非常惊人,just too good to be true.
那么倒底哪里错了?
原因出在argrelextrem的工作方式。它只有在股价已经向下走时,才能检测出一个顶点已经存在。这是一个未来函数。它意味着,只有在明天才可能识别出今天应该发出的信号。但我们不可能返回昨天来完成交易。
为了进一步验证上述观点,我们取2021年9月3日前后的两张图来对比:
上图是截止9月3日的图。从后面的结果来看,argrelextrem应该在这一天标注出一个底点。但是它并没有。
上图的时间是9月6日,这次它标注出来一个底点,但是标注在前一天。
由于均值线使用的是收盘价,这意味着,只有在9月6日收盘后,我们才能识别9月3日是一个低点。如果这时我们按9月6日的收盘价(实际交易还要晚一点,只能使用第二天的开盘价)来买入,情况会如何?
我们把上面的方法中,买卖价都顺延一天,得到的收益率是-13%。如果我们在起点时,以开盘价买入,在终点以收盘价卖出,则还可以收益8.4%。在动用高大上的数学方法后,成功地亏掉了21%。这里我们取得是5日均线。取10日、或者3日,都逃不掉亏损的命运。
理想是美好的,现实是残酷的。
然而。。。
老股民很容易观察到,在位置5附近,股价已经滞涨好几天了,均线也在走平。有句股谚,三天不创新高就要抛(这是对运做强势股而言)。这是因为,如果三天不创新高,则均线就会象上图中位置5附近一样走平,接下来既可能突破平台开启第二浪,也可能下跌。因此,我们的策略可以是这样,通过算法检测到均线走平,发出预警(此时可以部分减仓),等待明确的信号再决定是否清仓(反之则是买入)。
假设我们能在argrelextrem前一天发现走平的信号。如果此时处于高位,则卖出一半,待argrelextrem报出信号后,再卖出全部仓位。这样,上面函数的开仓、平仓部分就改为:
if buy and order is None:
order = {
"buy": close[x] * 0.5 + close[x+1] * 0.5,
"buy_at": bars["frame"][x+1]
}
elif sell and order:
buy = order["buy"]
sell = close[x+1] * 0.5 + close[x] * 0.5,
gain = sell / buy
order.update({
"sell": sell,
"sell_at": bars["frame"][x+1],
"gain": gain
})
这样我们得到的收益是5%。虽然不如买入并持有好,但毕竟这是我们凭实力挣的钱,而不是靠赌运气。所以这个收益可重复。
现在,问题就变成了,如何检测均线走平?这个问题回答起来也并不那么容易。不过我们先来看看这里的检测峰和谷的方法的实际作用。
从上面的分析可以看出,峰和谷的检测是后验的,即只有当峰和谷已经走出来,上面的算法才能检测到。虽然它对投资可能没有直接的指导意义,但我们也可以用它来标注数据,以供机器学习使用。
下图显示了使用上述方法,对上证指数一段时间的峰和谷进行标注的结果:

我们先是使用均线对波动进行平滑,然后通过argrelextrem进行峰和谷的检测,然后在此基础上,对峰和谷进行一些小的修正,使之与股价、而不是均线对齐。我们采用沪指过去6年的30分钟线,共发现734个顶点和738个底点,制作成为标注数据。
有了这些标注数据,我们就可以训练和检验自己的机器学习模型了。出人意料的是,数据量并不大。这也说明在A股市场上,趋势一旦形成,往往还能持续一段时间。这也是量化模型能够有用武之地的一个注脚。