课程  因子投资  机器学习  Python  Poetry  ppw  tools  programming  Numpy  Pandas  pandas  算法  hdbscan  聚类  选股  Algo  minimum  numpy  algo  FFT  模式识别  配对交易  GBDT  LightGBM  XGBoost  statistics  CDF  KS-Test  monte-carlo  VaR  回测  过拟合  algorithms  machine learning  strategy  python  sklearn  pdf  概率  数学  面试题  量化交易  策略分类  风险管理  Info  interview  career  xgboost  PCA  wavelet  时序事件归因  SHAP  Figures  Behavioral Economics  graduate  arma  garch  人物  职场  Quantopian  figure  Banz  story  量化传奇  rsi  zigzag  穹顶压力  因子  pe  ORB  策略  Xgboost  Alligator  Indicator  factor  alpha101  alpha  技术指标  wave  quant  algorithm  pearson  spearman  tushare  因子分析  Alphalens  涨停板  herd-behaviour  因子策略  momentum  因子评估  review  SMC  聪明钱  trade  history  indicators  zscore  波动率  强化学习  顶背离  freshman  resources  others  AI  DeepSeek  network  量子计算  金融交易  IBM  weekly  LLT  backtest  backtrader  研报  papers  UBL  quantlib  jupyter-notebook  scikit-learn  pypinyin  qmt  xtquant  blog  static-site  duckdb  工具  colors  free resources  barra  world quant  Alpha  openbb  数据  risk-management  llm  prompt  CANSLIM  Augment  arsenal  copilot  vscode  code  量化数据存储  hdf5  h5py  cursor  augment  trae  Jupyter  jupysql  pyarrow  parquet  数据源  quantstats  实盘  clickhouse  notebook  redis  remote-agent  AI-tools  Moonshot  回测,研报,tushare 

关于寻找顶和底的一些研究


Table of Content

scipy当中有一些信号处理函数,可以用来发现顶和底,比如argrelextrema。 比函数名上看,这是一个寻找相对极值点坐标的函数。我们来看一下它的用法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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定义为:
1
2
3
import numpy as np
def moving_average(ts, win):
    return np.convolve(ts, np.ones(win) / win, mode="valid")
它使用了卷积来求简单移动平均值。numpy的卷积运算速度很快,但如果想要更快的速度,可以使用bottleneckmove_mean,最高可以快6000多倍(取自官方文档,不一定是和np.convolve方法比较)。

通过给argrelextrem传入不同的比较函数(例子中是np.lessnp.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报出信号后,再卖出全部仓位。这样,上面函数的开仓、平仓部分就改为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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股市场上,趋势一旦形成,往往还能持续一段时间。这也是量化模型能够有用武之地的一个注脚。