课程  因子投资  机器学习  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 

papers »

RSRS 择时指标


Table of Content

RSRS 因子在 2005 年 3 月到 2017 年 3 月的上证 50 指数上,12 年总收益 1432.36%,年化 24.84%,夏普 1.42。同期指数收益仅为 290.13%。

该指标的大致思想是,将每日最高价与最低价分别视为阻力位与支撑位,把给定周期下线性回归拟合得到的斜率作为因子。斜率越陡,表示市场强度越强。

本文复现了 RSRS 因子,可运行的完整代码和数据在我们的研究平台中提供。如果你关心它的最新表现,或者任何一段时间的表现,只需要自己修改时间参数运行即可得到答案。


RSRS(Resistance Support Relative Strength) 因子是光大证券于 2017 年起的系列中,提出的一种择时因子。该系列最初发表于 2017 年,后来又在 2019 年和 2021 年对之前的因子构造进行了回顾和优化。这篇 notebook 将复现这一因子,并对其构造思想进行解读。

这是我们系列研报解读中的一篇。持续跟踪本系列文章,你将掌握复现研报所需要的理论知识、编程技巧、数据获取方案和交易策略经验,换言之,成为一名熟练的策略研究员。

在解读每一篇研报时,我们都会选附上研报原文:

然后才是我们对研报的解读和复现:

通过对比,你可以发现我们对研报进行了提炼和挖掘,主题思想更明确,也更容易读懂。

该策略的主要思想是:

Tip

  1. 最高价与最低价当日市场全体参与者博弈出来的真实阻力与支撑。
  2. 在相邻的两个时间点 [T0, T1] 之间,最高价 High 的变化值 \(\Delta_H = High(T1) - High(T0)\) 和最低价 Low 的变化值 \(\Delta_L = Low(T1) - Low(T0)\),两者的比值反应了阻力位与支撑位的相对强度,即 RSRS 指标。
  3. 为了过滤噪声,一般取\(T_1, T_2, ...T_n\)间的最高价和最低价进行线性回归,此时得到的斜率\(\beta\)即为 RSRS 指标,在本质上与定义 2 相同,即有以下公式:
\[ high = alpha + beta * low + epsilon, epsilon ~ N(0, sigma) \]

应该说策略的思想非常巧妙,也很符合我们的交易认知:如果市场投资者认为下面的支撑很强,就敢于向上试探更大的空间,因此最高价的涨幅就变大;如果市场投资者认为上方的压力很大,则更期望尽快脱手,从而导致最低价格的跌幅变大

研报作者还手绘了两幅图来说明这一思想:

研报还展示了如何为交易思想建模的一个简单但常用的技巧,即线性回归。线性回归是从简。丁伯格等人开创计量经济学以来,在经济和金融领域广泛使用的一种技巧。在这里,线性回归的引入,让交易思想得到合理的抽象,成为有统计实证支持的一个模型。

这个因子的计算方法如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import pandas as pd

def calc_rsrs_factor(df: pd.DataFrame, win: int = 18):
    df = df.copy()

    # 计算滑动窗口的协方差 Cov(low, high)
    rolling_cov = df["low"].rolling(window=win).cov(df["high"])

    # 计算滑动窗口的方差 Var(low)
    rolling_var = df["low"].rolling(window=win).var()

    df["RSRS"] = rolling_cov / rolling_var

    return df["RSRS"]

Tip

在这里我们使用了一个快速的向量化算法。如果你感到难以理解这个算法,那么,它对应的 vanilla 版本是这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
def calc_rsrs_vanilla(df, N):
        df = df.copy()
        temp = [np.nan] * N

        for row in range(len(df) - N):
            y = df['high'][row : row + N]
            x = df['low'][row : row + N]

            # 确保 x 和 y 的长度都为 N,并且没有 NaN 值
            if len(x) == N and len(y) == N and not x.isnull().any() and not y.isnull().any():
                beta = np.polyfit(x, y, 1)[0]
                temp.append(beta)
            else:
                temp.append(np.nan)

        df['rsrs'] = temp
        return df

在对比测试中,vanilla 版本的运行时间如果是 4.3ms 的话,那么向量化版本的运行时间则仅为 317us,快了 10 倍多。

现在,我们通过 tushare 的数据,来看一下因子计算的结果。下面的代码展示了如何获取 hs300 指数数据:

1
2
3
hs300 = pro.index_daily(ts_code = "000300.SH", start_date = "20250101", end_date = "20250601")
hs300.index = pd.to_datetime(hs300["trade_date"])
hs300 = hs300.sort_index(ascending=True)

Tip

在使用 tushare 的行情数据时,请务必按照这里的代码片段,先将 trade_date 设置为索引,然后进行排序,这样得到的数据的顺序,才会与大多数软件系统中的顺序一致。

现在,我们就可以计算出 RSRS 因子了:

1
2
hs300_factor = calc_rsrs_factor(hs300, 18)
hs300_factor

我们看到,hs300_factor 是一个 pd.Series,它的索引是日期,其值是因子值。这样的格式有利于后面合并横截面数据。

因子的质量如何?我们可以使用 Alphalens 来进行检验。Alphalens 是一个用于评估因子的库,简单易用,非常适合对因子进行快速评估。不过,它主要采用的是横截面评估法,因此,我们需要先获取沪深 300 指数中,所有成份股的历史数据,再计算出它们的因子值。

Tip

看了很多研报,但却不知道如何复现?2025 年,你应该加入匡醍研究平台,手把手带你复现研报。全年仅 360 元,即可享有超 100 份研报、复现代码及运行环境!在该运行环境中,提供了 Tushare 高级账户使用权(官方价格 500 元),仅此一项,就值回票价!

在我们的研究环境中,有从 2005 年到 2023 年所有个股的日线历史数据,可以允许我们进行较长周期的因子测试,并且已经有封装好的 alpha_test 方法可以调用。不过,我们还需要先获取沪深 300 成分股列表。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
df = pro.index_weight(
    index_code='000300.SH',
    start_date='20231201',
    end_date='20231231'
)

# 在研究环境中,我们使用的股票代码是以。XSHG 或者。XSHE 结尾的,所以,我们需要将股票代码转换一下。
def convert_symbol(x: str):
    if x.endswith(".SH"):
        return x.replace(".SH", ".XSHG")
    elif x.endswith(".SZ"):
        return x.replace(".SZ", ".XSHE")
    else:
        raise ValueError(f"{x}: not supported format")

universe = tuple(map(convert_symbol, df["con_code"].unique()))
universe[:5]

现在,我们就调用 alphatest 来进行因子测试:

1
2
3
4
start = datetime.date(2018, 1, 1)
end = datetime.date(2021, 12, 31)

_ = alphatest(universe, start, end, calc_rsrs_factor)

输出结果有点小意外,没有出现你期待的高收益,基至年化 Alpha 还是负的。不过,对 Alphalens 的输出我们需要辩证来看,这个简单的测试,已经说明了这个因子很可能是有 Alpha 存在的(它的 beta 接近于零),只是年化为负,这种情况下,我们只要修改因子的方向,即可得到正的年化收益。

不过,Alphatest 的结果与研报相去甚远,这应该作何解释呢?

原来,研报中 RSRS 指标的交易方式阈值买入卖出式。它要求先对过去 M 个交易日的斜率因子进行分层统计,取均值±一个标准差的值作为买入和卖出的阈值,所以,这是一种事件型交易方式,使用 Alphalens 就无法准确地回测出它的收益率。

Tip

Alphalens 默认的回测方式是横截面式的。尽管后期也加入了事件型回测,但支持的并不完善。也可能是考虑回测框架已经有了完善的事件回测机制,所以就没在这方面多下功夫?随着 Quantopian 的解散,今天我们已无从得知其中的缘由。

这里的 alphatest 是我们在《因子分析与机器学习策略》课程中开发的一个辅助函数,它在底层调用了 alphalens 的回测函数,但进行了一些简化,使得我们可以仅仅通过一行代码,就完成因子回测。

另一个区别是,研报使用交易标的是沪深 300 指数,它是由各个标的按一定权重构建出来的价格序列;当我们使用 alphalens 进行回测时,我们相当于构建了一个等权沪深 300 指数。这两个指数之间本来就会有差异。

现在,我们就回到研报的实现,对沪深 300 指数本身进行交易回测。

事件型交易策略必须要能给出交易信号。根据前面的叙述,这个交易信号是均值的一个标准差。为此,我们需要先对过去 M 天的 RSRS 进行统计,并计算出均值和标准差,然后进行 zscore 化。

不过,在开始之前,我们先对因子进行可视化,找一点点感觉。

 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
import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np
import scipy.stats as st

def describe(df, col, title):
    data = df[~df[col].isna()][col]

    # 创建图形
    fig, axes = plt.subplots(1, 2, figsize=(18, 9))
    plt.suptitle(title)

    # 基本统计量
    avg = data.mean()
    std = data.std()

    # 左侧:直方图
    sns.histplot(data, kde=False, stat='density', alpha=0.4, ax=axes[0])
    for line, color, label in zip(
        [avg, avg-std, avg+std],
        ['red', 'blue', 'blue'],
        ['Mean', '-1 Standard Deviation', '1 Standard Deviation']
    ):
        axes[0].axvline(x=line, color=color, linestyle='--', linewidth=0.8, label=label)
    axes[0].set_ylabel('Percentage', fontsize=10)
    axes[0].legend(fontsize=12)

    # 右侧:KDE 和正态分布拟合
    x = np.linspace(avg - 3*std, avg + 3*std, 100)
    kde = st.gaussian_kde(data)
    y_norm = st.norm.pdf(x, avg, std)

    axes[1].plot(x, kde(x), label='Kernel Density Estimation')
    axes[1].plot(x, y_norm, color='black', linewidth=1, label='Normal Fit')
    axes[1].axvline(x=avg, color='red', linestyle='--', linewidth=0.8, label='Mean')
    axes[1].set_ylabel('Probability', fontsize=10)
    axes[1].legend(fontsize=12)

    return plt.show()

# 调用函数
describe(hs300_factor.to_frame(), 'RSRS', '2018-2025 斜率数据分布')
斜率数据分布

根据研报和运行结果,卖出阈值在 0.8 附近,买入阈值在 1.0 附近。即如果 RSRS 指标大于 1.0,买入并持有;当 RSRS 回落到小于 0.8 时,就卖出。

如果我们以此确定阈值,就会犯了前视偏差的错误:我们把 2023 到 2025 的全部数据纳入了统计,但如果交易发生在 2023 年底呢?除非这些年来,RSRS 的分布一直保持不变,否则,我们就一定是参考了错误的阈值。

因此,我们需要以滑动窗口来确定交易阈值,即在\(T_0 ~ T_m\)个交易日中,找到 25%和 75%分位值,分别作为第 m 日买入或者卖出的阈值。在研报中,它换了另一种方式:将 high 对 low 的 N 日回归斜率,按 win 个窗口进行 z-score 化。在 z-score 化之后,如果当日因子值大于 0.7,则认为此处为买入信号;如果当日因子值小于-0.7,则认为此处为卖出信号。

Info

在标准正态分布中,数值 0.7 对应的分位数是 75.8%,负 0.7 对应的分位数是 24.9%。研报在这里没有严格遵循之前讲的±1 个标准差,可能是为了与 25%,75%这样的常用分位数对齐。

现在,我们按研报思想,对原始的 RSRS 因子进行滑动窗口处理。

 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
start = "20180101"
end = "20250601"

hs300 = pro.index_daily(ts_code = "000300.SH", start_date = start, end_date = end)
hs300.index = pd.to_datetime(hs300["trade_date"])
hs300 = hs300.sort_index(ascending=True)

def calc_rsrs(df: pd.DataFrame, win: int = 18):
    df = df.copy()

    # 计算滑动窗口的协方差 Cov(low, high)
    rolling_cov = df["low"].rolling(window=win).cov(df["high"])

    # 计算滑动窗口的方差 Var(low)
    rolling_var = df["low"].rolling(window=win).var()

    df["rsrs"] = rolling_cov / rolling_var

    return df

def calc_rsrs_zscored(df: pd.DataFrame, n: int = 18, m: int = 600):
    df = calc_rsrs(df, n)
    df["rsrs_"] = df["rsrs"].fillna(0)

    ZSCORE = (df['rsrs_'] - df['rsrs_'].rolling(m).mean()) / df['rsrs_'].rolling(m).std()
    df['rsrs_z'] = ZSCORE
    return df.drop(columns='rsrs_')

rsrs_z = calc_rsrs_zscored(hs300, 18, 600)

我们可以观察下 z-score 化后的因子:

1
describe(rsrs_z, 'rsrs_z', '2018-2025 Z-Score 化后的 RSRS 分布')

结果与前图(斜率数据分布)差别不大,此处从略。

现在我们来构建一个简单的交易策略:

 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
import matplotlib.dates as mdate
def RSRS_Strategy(start: datetime.date, end: datetime.date, n: int=18, m: int=600):
    start_ = start.strftime("%Y%m%d")
    end_ = end.strftime("%Y%m%d")

    data = pro.index_daily(ts_code = "000300.SH", start_date = start_, end_date = end_)
    data.index = pd.to_datetime(data["trade_date"])
    df = data.sort_index(ascending=True)

    rsrs_z = calc_rsrs_zscored(df, n, m)  # 计算标准分指标

    # 需要扣除前期计算的 600 日
    rsrs_z=rsrs_z[max(n, m):]

    print('回测起始日:',min(rsrs_z.index))

    z_singal = []
    threshold = 0.7
    for row in range(len(rsrs_z)):
        if rsrs_z['rsrs_z'][row] > threshold:
            z_singal.append(1)

        else:
            if row != 0:
                if z_singal[-1] and rsrs_z['rsrs_z'][row] > -threshold:
                    z_singal.append(1)
                else:
                    z_singal.append(0)
            else:
                z_singal.append(0)

    # 交易信号
    rsrs_z['z_singal'] = z_singal

    # 每日收益
    rsrs_z['ret'] = rsrs_z['close'].pct_change()

    # 累积净值
    z_cum = (1+rsrs_z['z_singal']*rsrs_z['ret']).cumprod()

    # 基准净值
    benchmark = (1+rsrs_z['ret']).cumprod()

    # 画图
    plt.figure()
    fig = plt.figure(figsize=(20, 10))
    ax1 = fig.add_subplot(1, 1, 1)

    ax1.plot(z_cum, label='RSRS 策略')
    ax1.plot(benchmark, label='沪深 300')

    ax1.xaxis.set_major_formatter(mdate.DateFormatter('%Y-%m'))
    plt.legend(loc='best')
    plt.xlabel('时间')
    plt.ylabel('净值')
    plt.title('RSRS 指标策略净值曲线')
    plt.show()

    return z_cum, benchmark

strategy, benchmark = RSRS_Strategy(
    datetime.date(2005, 1, 1), datetime.date(2018, 1, 1), m=300
)

研报发表于 2017 年,我们使用了 2005 年到 2018 年间共 11 年的数据进行了回测,从简单的净值曲线来看,策略以 10 倍的涨幅,远远超过了基准模型,与研报结果相接近。


本文部分代码参考了 Hugo2046 的 github 项目,特此鸣谢。