跳转至


课程  因子投资  机器学习  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  穹顶压力  因子  ESG  因子策略  投资  策略  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 

strategy »

『译研报03』Z变换改造均线,一个12年前的策略为何仍能跑赢大盘?


传统移动平均线(MA)是技术分析中常用的趋势跟踪指标,通过对股票价格或指数在一定天数内的平均值进行计算,以刻画其变动方向。MA 的计算天数越多,其平滑性越好,但随之而来的时滞(延迟)影响也越严重。这意味着 MA 指标在跟踪趋势时容易出现“跟不紧”甚至“跟不上”的情况,平滑性和延迟性成为其不可避免的矛盾。

低延迟趋势线(LLT)的构造借鉴了信号处理理论中的滤波方法。传统的 EMA 指标被视为一阶低通滤波器,但其滤波效果相对较差,通带和阻带间的过渡带太长。LLT 通过设计二阶滤波器来优化滤波效果,实现了对信号高频部分的有效过滤,同时保留了低频部分的强度。与 MA 和 EMA 均线相比,LLT 大幅降低了延迟,同时兼顾了趋势线的平滑性,从而克服了传统 MA 指标在跟踪趋势时的滞后问题。

本文取材于广发证券-《低延迟趋势线与交易择时》一文。原文仅回测到2013年。我们在此基础上回测到2024年底,发现它在多空组合时,仍有很好的表现(上证夏普1.33)。

策略档案之LLT

LLT Benchmark Strategy
Start Period 2013-01-04 2013-01-04
End Period 2024-12-31 2024-12-31
Cumulative Return 47% 345%
CAGR﹪ 2% 9%
Sharpe 0.27 1.33

传统均线系统

为了对比,我们先给出传统均线的定义及图形。

移动平均(Moving Average)线,其算法为:

\(MA(n) = \frac{1}{n}\sum^{n-1}_{i=0}price(T-i)\)

其中 price 一般选择收盘价,MA(n) 即为 T 日的 n 日均线指标。对于 MA 指标,n 越大,趋势线的平滑性越好。

基于移动均线,我们可以实现一个简单的趋势跟随策略。信号的判断方式是看移动平均线的切线。如果切线斜率向上,则多头持有;如果切线斜率向下,则多头卖出。

下面的代码演示了 5, 10, 30 和 60 日均线。其中 30 日均线被称为生命线。最后的绘图中,显示了 30 日均线上,切线由正转负的情况。

 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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
def get_price(symbol, start_date, end_date):
    pro = pro_api()

    price_df = pro.index_daily(
        ts_code=symbol,
        start_date=start_date.strftime("%Y%m%d"),
        end_date=end_date.strftime("%Y%m%d"),
    )

    price_df = (
        price_df.rename({"trade_date": "date", "ts_code": "asset"}, axis=1)
        .sort_values("date", ascending=True)
        .set_index("date")
    )

    return price_df[["close"]]

start = datetime.date(2012, 10, 26)
end = datetime.date(2013, 4, 9)

price_df = get_price("000001.SH", start, end)

for i in [5, 10, 30, 60]:
    price_df[f"MA%d" % i] = price_df["close"].rolling(i).mean()

# 计算生命线趋势拐点
price_df["slope_30_5"] = price_df["MA30"].rolling(5, min_periods=5).apply(lambda y: np.polyfit(np.arange(5), y, 1)[0])

price_df["slope_30_5"] = price_df["slope_30_5"].fillna(0)
signs = np.sign(price_df["slope_30_5"])
sign_changes = signs * signs.shift(1) == -1

revert_dates = price_df.index[sign_changes]
print("找到的反转日期:", [i for i in revert_dates])

# 在现有图表上添加切线
cols = ["MA5", "MA10", "MA30", "MA60"]
ax = price_df[cols].plot(figsize=(18, 8), title='30 日均线切线分析')

# 切线长度
tangent_length = 15

for dt in revert_dates:
    # 获取该位置的 MA 值和斜率
    ma_value = price_df.loc[dt, 'MA30']
    slope = price_df.loc[dt, 'slope_30_5']
    i = price_df.index.get_loc(dt)

    # 计算切线范围
    start_idx = max(0, i - tangent_length)
    end_idx = min(len(price_df), i + tangent_length + 1)

    # 计算切线坐标
    x_offset = np.arange(start_idx - i, end_idx - i)
    y_tangent = ma_value + slope * x_offset
    tangent_dates = np.arange(start_idx, end_idx)

    # 绘制切线
    color = 'green' if slope > 0 else 'red'
    linestyle = '-.' if slope > 0 else '--'

    ax.plot(tangent_dates, y_tangent, 
           color=color, linestyle=linestyle, linewidth=2, alpha=0.8)

    # 标记切点
    ax.scatter(i, ma_value, color=color, s=100, zorder=5)
    ax.annotate(dt, 
               xy=(i, ma_value),
               xytext=(5, 10), textcoords='offset points',
               fontsize=9, color=color,
               bbox=dict(boxstyle='round,pad=0.3', facecolor='white', alpha=0.8))

plt.show()

在该图中,绿色线为30日线。红点处,30日均线斜率由正转负,即为卖出信号。我们看到,通过 30 日均线的趋势跟随,可以捕捉到大的波段行情。但是,当趋势线提示我们该下车时,已经离行情高点下降不少。但如果使用 5 日均线这种短一点的均线,又会导致频繁发出信号,增加交易成本的情况。

这个结果表明,传统均线存在窗口小时,均线不平滑,趋势线切线上下抖动现象严重;窗口大时,均线平滑性好,但滞后性较强的问题。

Tip

寻找切线反转点(即切线斜率由正变负,或者由负变正)时,比较有技巧。这个技巧在很多场合下都会遇到:

1
2
3
4
5
    price_df["slope_30_5"] = price_df["slope_30_5"].fillna(0)
    signs = np.sign(price_df["slope_30_5"])
    sign_changes = signs * signs.shift(1) == -1

    revert_dates = price_df.index[sign_changes]

如何解决传统均线在延迟和平滑上,存在鱼与熊掌不可兼得的矛盾?

LLT 均线

研报在 LLT 均线的原理及推导上介绍得比较深入细致。但是,要理解 LLT,需要有 Z 变换等基础,我们简单解释如下:

\[\frac{LLT(z)}{price(z)} = \frac{(\alpha-\alpha^2/4) + (\alpha^2/2)z^{-1} - (\alpha-3\alpha^2/4)z^{-2}}{1-2(1-\alpha)z^{-1} + (1-\alpha)^2z^{-2}}\]

这是一个所谓的Z域上的公式,我们需要按相应的规则,变换为时域公式。它的变换规则是:

  • 上式是一个二阶 IIR 滤波器的传递函数。要得到时域递推公式,需要将分子分母都乘以分母的表达式,使分母变为 1(即左边只剩 LLT(z)),右边是分子的多项式与 price(z) 的卷积: \(\(LLT(z) \cdot [1-2(1-\alpha)z^{-1} + (1-\alpha)^2z^{-2}] = price(z) \cdot [(\alpha-\alpha^2/4) + (\alpha^2/2)z^{-1} - (\alpha-3\alpha^2/4)z^{-2}]\)\)
  • 展开后,按 z 变换的性质,将 z{-1}、z分别对应到时域的 t-1、t-2 期: \(\(LLT_t - 2(1-\alpha)LLT_{t-1} + (1-\alpha)^2LLT_{t-2} = (\alpha-\alpha^2/4)price_t + (\alpha^2/2)price_{t-1} - (\alpha-3\alpha^2/4)price_{t-2}\)\)
  • 移项得到递推公式: \(\(LLT_t = (\alpha-\alpha^2/4)price_t + (\alpha^2/2)price_{t-1} - (\alpha-3\alpha^2/4)price_{t-2} + 2(1-\alpha)LLT_{t-1} - (1-\alpha)^2LLT_{t-2}\)\)

变换后,我们最终得到的公式为:

\[ y_t = (\alpha-\alpha^2/4)x_t + (\alpha^2/2)x_{t-1} - (\alpha-3\alpha^2/4)x_{t-2} + 2(1-\alpha)y_{t-1} - (1-\alpha)^2y_{t-2} \]

可以看到,最终公式是一个递归函数,这里的\(y_t\)即为我们要求的LLT,它由前两期的LLT与最近三期的价格、以及一个\(\alpha\)参数共同决定。

它的实现代码是:

 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
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

def calculate_llt(prices, alpha=0.05):
    """
    计算LLT (Linearly Weighted Least Squares Triangular) 均线

    参数:
    prices (array-like): 价格序列
    alpha (float): 平滑系数,范围(0,1),值越小均线越平滑,滞后性越大

    返回:
    array: LLT均线序列
    """
    n = len(prices)
    llt = np.zeros(n)

    # 初始化前两个值
    if n >= 1:
        llt[0] = prices[0]
    if n >= 2:
        llt[1] = prices[1]

    # 计算系数
    a1 = alpha - (alpha**2) / 4
    a2 = (alpha**2) / 2
    a3 = alpha - 3 * (alpha**2) / 4
    a4 = 2 * (1 - alpha)
    a5 = - (1 - alpha)**2

    # 递归计算LLT
    for t in range(2, n):
        llt[t] = a1 * prices[t] + a2 * prices[t-1] - a3 * prices[t-2] + a4 * llt[t-1] + a5 * llt[t-2]

    return llt

下面,我们对比一下EMA, 5日均线以及LLT均线:

1
2
3
4
5
6
7
8
price_df = get_price("000001.SH", start, end)
price_df['EMA'] = price_df['close'].ewm(alpha=0.05, adjust=False).mean()


price_df['MA30'] = price_df['close'].rolling(30).mean()
price_df['LLT'] = calculate_llt(price_df['close'], 0.05)

price_df.plot(figsize=(18,8),title='各类趋势线比较')

从这个个例上看,LLT确实跟得更紧(接近MA5),同时做到了比较平滑(比EMA更平滑,接近MA30)。

在计算LLT时,有一个\(\alpha\)参数,它的取值对平滑程度和延迟性都有影响:

1
2
3
4
5
6
for a in [0.03,0.04,0.05]:
    price_df[f'LLT(%s)'%a] = calculate_llt(price_df['close'],a)


show_cols = ["LLT(0.03)", "LLT(0.04)", "LLT(0.05)"]
price_df[show_cols].plot(figsize=(18,9),title='不同α参数的LLT趋势线')

回测与对比

我们先来看看传统均线的回测情况。我们将定义一个交易函数,它接受一个dataframe,因子列名和计算slope的窗口。后面我们在对LLT进行回测时,我们将使用同一个函数,这样可以确保对比的公正性。

 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
59
60
61
import quantstats as qs
mport quantstats as qs
def trading_strategy(df, factor_col: str, slope_window=5, long_weight=0.5, short_weight=0.5):
    """
    计算基于均线斜率的多空组合策略收益

    参数:
    price_df: 包含收盘价的DataFrame
    factor_col: 因子列
    slope_window: 计算斜率的窗口大小
    long_weight: 多头仓位权重 (0-1)
    short_weight: 空头仓位权重 (0-1)

    返回:
    DataFrame: 包含策略收益的DataFrame
    """
    df = df.copy()
    df['slope'] = (df[factor_col].rolling(slope_window)
                    .apply(lambda x: np.polyfit(np.arange(slope_window), x, 1)[0]))

    df['signal'] = 0
    df.loc[df['slope'] > 0, 'signal'] = 1
    df.loc[df['slope'] < 0, 'signal'] = -1

    # 计算每日收益率
    df['benchmark'] = df['close'].pct_change()

    # 计算多空组合收益
    df['long_return'] = np.where(df['signal'] == 1, df['benchmark'], 0)
    df['short_return'] = np.where(df['signal'] == -1, -df['benchmark'], 0)

    # 组合收益 = 多头收益 * 多头权重 + 空头收益 * 空头权重
    df['strategy'] = df['long_return'] * long_weight + df['short_return'] * short_weight

    return df


def backtest_ma(start, end, win:int=30):
    price_df = get_price("000001.SH", start, end)
    factor_col = f"ma{win}"
    price_df[factor_col] = price_df["close"].rolling(win).mean()

    strategy_df = trading_strategy(price_df, factor_col)
    strategy_df.index = pd.to_datetime(strategy_df.index)

    qs.plots.returns(
                returns=strategy_df["strategy"],
                benchmark=strategy_df["benchmark"]
            )

    metrics = qs.reports.metrics(
            returns = strategy_df["strategy"],
            benchmark = strategy_df["benchmark"],
            display=False
        )

    print(metrics[:10])

start = datetime.date(2005, 9, 6)
end = datetime.date(2013, 6, 28)
backtest_ma(start, end)

我们在30日均线上,回测得到的结果,与研报基本一致,都在300%左右。

Tip

理论上,知道回测时间,我们应该能做到像素级的复现。但是,这里有一个重要的参数,即求斜率时的窗口大小,研报没有披露。我们在复现时,使用的窗口为5。

所以,在对LLT进行回测时,我们只需要先计算出LLT,再将LLT作为因子传入 trading_strategy 函数即可。

 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
def backtest_llt(start, end, win:int=30, d=20):
    price_df = get_price("000001.SH", start, end)

    alpha = 2 / (d + 1)
    price_df["llt"] = calculate_llt(price_df["close"], alpha)

    strategy_df = trading_strategy(price_df, "llt")
    strategy_df.index = pd.to_datetime(strategy_df.index)

    qs.plots.returns(
                returns=strategy_df["strategy"],
                benchmark=strategy_df["benchmark"]
            )

    metrics = qs.reports.metrics(
            returns = strategy_df["strategy"],
            benchmark = strategy_df["benchmark"],
            display=False
        )

    print(metrics[:10])

start = datetime.date(2005, 1, 1)
end = datetime.date(2012, 10, 1)
backtest_llt(start, end)

运行结果表明,改用LLT均线之后,在同样的时间段,累积收益高出100%,夏普也由1.35提升到1.61,超额非常明显。

以上是研报复现的情况。研报发表时间较早,因此只回测到2013年,在此之后,情况如何?这个策略是否仍然有效?

如果把回测时间改到2013年之后,2024年底之前,保持alpha仍为0.05, 我们发现收益率对benchmark,仍有明显的超额。考虑到2015年之后,A股就一直没有大的行情,作为趋势跟随策略,收益不可能象之前那么好,也是理所当然。

不过,考虑增加alpha值(对应ema线窗口缩短)为0.1,以捕捉更短的波段,增加交易机会之后,则仍可以得到非常漂亮的超额和夏普:

1
2
3
start = datetime.date(2013, 1, 1)
end = datetime.date(2024, 12, 31)
backtest_llt(start, end, d = 10)

如果进一步增加alpha值到0.17(对应d=5)左右,则从2014年之后,上证指数多空组合收益达到了345%,夏普达到了1.33!

本策略全部代码以notebook格式发布在Quantide Research平台。加入平台后,可自行运行和验证此策略。