RSRS回测最容易错在哪里?用Python处理历史行情、复权

用户头像sh_***416jmt75L
2026-09-07 发布

很多人第一次用 Python 写 RSRS,真正卡住的不是线性回归,而是回测结果到底能不能相信。

代码可能没有报错,RSRS 曲线也能正常画出来,但只要下面任意一个环节处理不严谨,结果就可能失真:

  • 历史 K 线没有正确排序;
  • RSRS 标准化窗口混入未来数据;
  • 当天收盘计算的信号被当天价格执行;
  • 复权口径前后不一致;
  • 回测收益没有考虑实际交易时点。

RSRS 本质上是一个高度依赖时间序列的指标。因此,与其先讨论“哪个参数收益最高”,不如先把数据管道和时间轴处理正确。

先把RSRS拆成数据问题和策略问题

一个完整的 RSRS 回测可以拆成四层:

历史行情
   ↓
高低价回归
   ↓
RSRS指标
   ↓
交易信号
   ↓
策略收益

这四层不要写成一个巨大函数。

例如:

def load_price_data():
    ...

def calculate_rsrs():
    ...

def generate_signal():
    ...

def backtest():
    ...

这样做的直接好处是:当回测结果异常时,可以快速判断到底是数据问题、指标问题还是交易逻辑问题。

第一关:历史行情必须先排序

假设 DataFrame 是:

trade_date       high       low       close
2026-01-05       ...        ...       ...
2026-01-06       ...        ...       ...
2026-01-07       ...        ...       ...

在任何 rolling、shift 或回归之前,都应该显式排序:

df["trade_date"] = pd.to_datetime(df["trade_date"])

df = (
    df.sort_values("trade_date")
      .drop_duplicates("trade_date")
      .reset_index(drop=True)
)

不要依赖数据源“通常会按时间返回”。

因为 RSRS 使用:

rolling()
shift()
iloc[]

这些操作全部依赖行顺序。

如果时间顺序反了,Python 仍然可能正常运行,只是计算出来的指标已经失去了意义。

第二关:RSRS的N和M其实是两个不同的问题

以常见的参数设置为例:

N = 18
M = 600

N 用来回答:

最近这一小段行情的高低点关系是什么?

M 用来回答:

当前 β 相对于更长历史中的 β 处于什么位置?

所以:

N = 18

并不意味着整个 RSRS 只需要 18 天数据。

如果还要计算:

M = 600

那么在策略真正产生稳定的标准化信号之前,需要准备远多于 600 个交易日的数据。

这也是为什么实际研究中应该区分:

数据预热区间
策略正式回测区间

而不是直接从回测起始日开始计算指标。

为什么“预热数据”很重要?

假设正式回测从:

2020-01-01

开始。

但 RSRS 需要:

N = 18
M = 600

那么不能只下载 2020 年之后的数据,然后第一天就开始生成信号。

更合理的结构是:

历史预热数据
       ↓
计算 β
       ↓
建立 β 历史序列
       ↓
计算 Z-score
       ↓
进入正式回测区间

这样可以避免正式回测前期因为数据不足而产生大量 NaN 或不稳定的标准化值。

第三关:复权方式不能随意混用

RSRS 使用的是:

High
Low

因此复权方式会直接影响回归输入。

如果你的历史价格使用前复权,那么:

high
low
close

最好保持相同的价格口径。

不要出现:

high → 前复权
low  → 后复权
close → 不复权

然后再把三者放到同一个策略里。

QuantDash 当前 Python SDK 的公开文档明确支持 K 线复权方式 forwardbackwardnone

因此在数据层应该明确记录:

adjust = "forward"

或者:

adjust = "none"

而不是让不同代码模块各自决定复权方式。

第四关:不要让当天信号直接吃到当天收益

这是 RSRS 回测最重要的时间问题之一。

假设:

T日:
收盘后计算RSRS

那么这个信号真正能够影响的交易,至少应该是:

T+1日

而不是 T 日本身。

例如:

df["signal"] = (
    df["rsrs"] > 0.7
).astype(int)

df["position"] = df["signal"].shift(1)

df["market_return"] = df["close"].pct_change()

df["strategy_return"] = (
    df["position"]
    * df["market_return"]
)

这里的:

shift(1)

就是把信号和收益错开。

如果策略实际定义为盘中某个时间生成信号,那么应该进一步根据具体行情频率和执行时间设计,而不是简单照搬日线版本。

RSRS不是“一个公式”,而是一条计算链

以修正标准分为例:

βt=OLS(High,Low)\beta_t = OLS(High,Low)然后:

Zt=βt−μβ,tσβ,tZ_t= \frac{\beta_t-\mu_{\beta,t}} {\sigma_{\beta,t}}再:

RSRSt=Zt×Rt2RSRS_t=Z_t\times R_t^2每一步都可能产生数据问题。

例如:

beta

为空,可能是历史窗口不足。

z

为空,可能是 β 的标准化窗口不足。

r2

异常,可能是窗口内价格没有足够变化。

因此不要简单地:

df.fillna(0)

把所有问题抹掉。

更好的方式是先知道 NaN 为什么产生,再决定是否应该等待更多历史数据。

用Python把RSRS计算封装起来

可以把单日计算写成一个纯函数:

import numpy as np


def calculate_rsrs(high, low, n=18):
    high = np.asarray(high, dtype=float)
    low = np.asarray(low, dtype=float)

    if len(high) < n:
        return np.nan, np.nan

    x = low[-n:]
    y = high[-n:]

    if np.isnan(x).any() or np.isnan(y).any():
        return np.nan, np.nan

    beta, alpha = np.polyfit(x, y, 1)

    fitted = alpha + beta * x

    ss_res = np.sum((y - fitted) ** 2)
    ss_tot = np.sum((y - y.mean()) ** 2)

    if ss_tot == 0:
        return beta, np.nan

    r2 = 1 - ss_res / ss_tot

    return beta, r2

再对历史数据滚动计算。

这种结构虽然代码比“全部写在一个循环里”稍长,但对于量化研究更容易排查。

数据量比较大时,不要把行情获取和回测逻辑绑死

假设你要研究的不只是一个指数,而是:

沪深300
中证500
上证50
多个ETF

这时最好让数据层先生成统一 DataFrame,再让 RSRS 模块处理。

例如:

Data API
   ↓
统一 trade_date / high / low / close
   ↓
Pandas DataFrame
   ↓
RSRS calculator
   ↓
Signal
   ↓
Backtest

QuantDash 当前公开 Python SDK 支持直接返回 DataFrame 的日 K 线,也支持多标的批量 K 线获取。

例如:

from quantdash import QuantDash

qd = QuantDash()

symbols = [
    "600519.SH",
    "000001.SZ",
    "601318.SH",
]

dfs = qd.klines.batch(
    symbols,
    period="1d",
    count=1000,
    to_dataframe=True,
    show_progress=True,
)

官方 PyPI 文档目前给出的批量接口返回的是:

{
    "600519.SH": DataFrame,
    "000001.SZ": DataFrame,
    "601318.SH": DataFrame,
}

因此后面的策略研究仍然可以完全保持在 Pandas 层。

这里 QuantDash 解决的是行情数据获取与 DataFrame 接入,并不负责 RSRS 的指标计算,也不负责你的回测策略。

如果只是研究一个指数,是否需要商业数据API?

不一定。

如果你的任务只是:

学习 RSRS 原理 + 写一个 Python 示例 + 偶尔做历史研究

本地 CSV、已有数据库或者其他适合你的免费数据源都可以。

真正需要关注数据源的,是当研究变成长期运行的工程之后:

每天自动更新
        ↓
历史数据持续追加
        ↓
字段保持一致
        ↓
代码格式统一
        ↓
缺失数据处理
        ↓
Pandas继续计算

此时,数据获取代码本身就成为系统的一部分。

QuantDash 的价值主要在这一层:当前公开资料显示,其 Python SDK 面向 A 股、ETF、美股和港股市场,提供 K 线、实时行情等数据接口,并支持 DataFrame 接入。

如果你的策略只需要一个指数的低频历史数据,那么不一定值得为了 RSRS 本身增加新的数据依赖;如果你正在把多个市场或多个标的的数据接入统一研究管道,统一接口的价值会更明显。

回测收益应该怎么看?

RSRS 回测最终通常会关注:

累计收益
年化收益
最大回撤
波动率
Sharpe
交易次数
空仓比例

但这些数字必须来自实际运行的数据和明确的回测假设。

尤其不能因为某篇公开 RSRS 文章出现过某个收益率,就把那个数字直接套到自己的策略上。公开资料中不同实现使用的窗口、阈值、标的、回测区间和交易规则并不相同,因此结果不能直接横向复制。

更应该先把回测定义写清楚:

标的:某个宽基指数或ETF
频率:日线
N:18
M:600
买入阈值:策略参数
卖出阈值:策略参数
执行:下一交易日
复权:明确指定
手续费:明确指定
滑点:明确指定

然后再解释收益结果。

否则“RSRS 年化收益多少”这个问题本身就缺少上下文。

一个实用的RSRS排查顺序

如果你的回测结果明显异常,可以按这个顺序排查:

1. 先看交易日期

assert df["trade_date"].is_monotonic_increasing

2. 再看OHLC是否存在空值

print(
    df[["high", "low", "close"]]
    .isna()
    .sum()
)

3. 检查N日回归

确认每个 β 是否只使用过去 N 个交易日。

4. 检查M日标准化

确认 Z-score 没有使用未来 β。

5. 检查信号和持仓

df[["trade_date", "rsrs", "signal", "position"]].tail(20)

6. 最后才检查收益

df["strategy_return"].describe()

这个顺序比看到收益率异常之后直接修改 RSRS 阈值更有效。

因为很多所谓“策略参数问题”,其实是数据时间轴问题。

RSRS真正适合放在量化系统的哪一层?

如果把整个策略拆成工程模块:

┌──────────────────────┐
│      行情数据层       │
│ K线 / 复权 / 标的代码 │
└──────────┬───────────┘
           ↓
┌──────────────────────┐
│      指标计算层       │
│ β / R² / Z-score     │
└──────────┬───────────┘
           ↓
┌──────────────────────┐
│      信号生成层       │
│ 买入 / 持有 / 空仓    │
└──────────┬───────────┘
           ↓
┌──────────────────────┐
│      回测逻辑层       │
│ 收益 / 回撤 / 成本    │
└──────────────────────┘

RSRS 属于中间的指标与信号层

数据源只负责把可靠的市场数据送进来;策略研究代码负责把数据转换成指标和信号;回测代码再根据交易规则计算策略表现。

把这几层分开之后,以后即使更换行情数据源,也不需要重写整个 RSRS 策略。

总结

RSRS 用 Python 实现并不复杂,真正容易出问题的是回测工程:

先保证时间轴正确,再保证指标窗口正确,最后才讨论策略参数和收益。

对于 RSRS,至少应该明确:

  • N 日高低价回归怎么计算;
  • M 日 β 标准化怎么计算;
  • 是否使用 ZZ×R² 或右偏版本;
  • 信号在什么时候产生;
  • 信号对应哪一天执行;
  • 历史行情采用什么复权方式;
  • 回测区间前是否准备了足够的预热数据。

如果使用 QuantDash,最合理的定位也是把它放在数据层:利用其当前公开的 Python SDK 获取历史 K 线并直接进入 Pandas,RSRS 计算、信号生成和回测仍由自己的研究代码完成。

QuantDash 官方资源

评论