第 6 章 · 06 RPS 相对强度:全市场横向排位 本节摘要:本节精读 Sequoia-X 最具「横向视野」的策略——RPS 相对强度(Relative Price Strength)。RPS 是欧奈尔在《笑傲股市》中提出的核心概念:先纵向算涨幅,再横向排百分位——选出「同期涨幅跑赢 X% 市场」的强势股。A 股每天 5000+ 只股票都在涨,绝对涨幅不说明问题——只有「在全市场的相对位置」才能识别真强者。Sequoia-X 用四个步骤实现 RPS:① 算每只股票 120 日涨幅 → ② 取最近一个交易日的全市场数据 → ③ 横向排位 → ④ 取 RPS ≥ 90 且接近 120 日阶段高点的标的。
本节摘要:本节精读 Sequoia-X 最具「横向视野」的策略——RPS 相对强度(Relative Price Strength)。RPS 是欧奈尔在《笑傲股市》中提出的核心概念:先纵向算涨幅,再横向排百分位——选出「同期涨幅跑赢 X% 市场」的强势股。A 股每天 5000+ 只股票都在涨,绝对涨幅不说明问题——只有「在全市场的相对位置」才能识别真强者。Sequoia-X 用四个步骤实现 RPS:① 算每只股票 120 日涨幅 → ② 取最近一个交易日的全市场数据 → ③ 横向排位
rank(pct=True) * 100→ ④ 取 RPS ≥ 90 且接近 120 日阶段高点的标的。本节会讲清 RPS 的数学原理、四个步骤的工程实现、Sequoia-X 与欧奈尔原版的差异、几个常见误区,最后用一段可运行的代码让你亲手验证「横向排位」的过程。
内容来源:原项目 RPS 突破策略模块精读 + 欧奈尔《笑傲股市》公开文献。
⚠️ 风险提示:RPS 在「牛市/反弹市」最有效——熊市中所有 RPS 都会向下。单边下跌市 RPS 几乎全部信号失真。
阅读完本节,你应当能够:
groupby + rank(pct=True) 的横向排位实现。一个朴素的问题:「涨幅 30%」是一只强势股吗? 单看「30%」这个数字,不能回答:
绝对涨幅没法横向对比。RPS 的核心价值就是「在比较中识别强者」:
RPS = 个股涨幅在「全市场同期涨幅分布」中的百分位
数学定义:
RPS 取值 0~100,RPS = 90 表示「跑赢 90% 的市场」。
RPS 计算分「纵向」与「横向」两步:
每只股票独立算自己的 120 日涨幅(纵向——自己跟自己比)。
所有股票的涨幅放在一张表里,横向排位(自己跟别人比):
pandas 的 rank(pct=True) 直接给出「百分位」(0~1),乘以 100 即为 RPS。
只保留 RPS ≥ 90 的标的——「同期涨幅跑赢 90% 市场」的强势股。
RPS 90+ 还要「接近阶段高点」——close >= 120 日滚动最高价 × 0.9——保证「既强、又在突破」。
第 4 步是 Sequoia-X 在欧奈尔基础上的改进——避免选在「已涨完」的顶部。
def run(self) -> list[str]: # 1. 一次性从 SQLite 读全市场行情 with sqlite3.connect(self.engine.db_path) as conn: df = pd.read_sql("SELECT symbol, date, close, high FROM stock_daily", conn) df['date'] = pd.to_datetime(df['date']) df = df.sort_values(['symbol', 'date']) # 2. 纵向:每只股票算 120 日涨幅 df['close_shift'] = df.groupby('symbol')['close'].shift(120) df['pct_change'] = (df['close'] - df['close_shift']) / df['close_shift'] # 3. 取最近一个交易日的全市场快照 latest_date = df['date'].max() latest_df = df[df['date'] == latest_date].copy() latest_df = latest_df.dropna(subset=['pct_change']) # 4. 横向:全市场排百分位 latest_df['rps'] = latest_df['pct_change'].rank(pct=True) * 100 strong_stocks = latest_df[latest_df['rps'] >= 90].copy() # 5. 突破确认:接近 120 日阶段高点 roll_high = df.groupby('symbol')['high'].rolling( window=120, min_periods=60 ).max().reset_index(level=0, drop=True) df['roll_high'] = roll_high latest_roll_high = df[df['date'] == latest_date][['symbol', 'roll_high']] strong_stocks = strong_stocks.merge(latest_roll_high, on='symbol') # 6. 收盘价 ≥ 滚动最高 × 0.9 breakout = strong_stocks['close'] >= strong_stocks['roll_high'] * 0.9 selected = strong_stocks[breakout] return selected['symbol'].tolist()
逐段精读:
| 行 | 作用 |
|---|---|
SELECT ... FROM stock_daily |
一次性读全市场行情,避免 5000 次单股查询 |
groupby('symbol')['close'].shift(120) |
纵向:每只股票 120 日前的收盘价 |
df['pct_change'] = ... / df['close_shift'] |
算 120 日涨幅 |
df['date'] == df['date'].max() |
取最近一个交易日(全市场同一日) |
rank(pct=True) * 100 |
横向:全市场排百分位(核心) |
>= 90 |
阈值过滤:只保留 RPS ≥ 90 |
groupby('symbol')['high'].rolling(120, ...) |
每只股票的 120 日滚动最高价 |
>= roll_high * 0.9 |
突破确认:接近阶段高点 |
rank(pct=True) 的精妙rank(pct=True) 是 RPS 实现的灵魂——它把「绝对数值」变成「相对位置」:
# 假设 5 只股票的涨幅 pct_changes = pd.Series([0.10, 0.25, 0.05, 0.50, 0.30]) # 横向排位(pct=True 给百分位) pct_changes.rank(pct=True) * 100 # 输出:[40, 60, 20, 100, 80] # 解释:跑赢 40% / 60% / 20% / 100% / 80% 市场
| 涨幅 | 排名 | 百分位 | 含义 |
|---|---|---|---|
| 0.50 | 1/5 | 100% | 跑赢 100%(最强) |
| 0.30 | 2/5 | 80% | 跑赢 80% |
| 0.25 | 3/5 | 60% | 跑赢 60% |
| 0.10 | 4/5 | 40% | 跑赢 40% |
| 0.05 | 5/5 | 20% | 跑赢 20%(最弱) |
rank(pct=True)默认升序——值越大排名越高、百分位越大。所以RPS 高 = 涨幅大 = 强势。
下面这段代码用合成数据演示 RPS 的四个步骤:
# rps_demo.py # 可运行:RPS 相对强度计算 import numpy as np import pandas as pd def calc_rps(df: pd.DataFrame, rps_period: int = 120, rps_threshold: int = 90) -> pd.DataFrame: """计算 RPS 并选出强势股。""" # 1. 纵向:算每只股票 120 日涨幅 df["close_shift"] = df.groupby("symbol")["close"].shift(rps_period) df["pct_change"] = (df["close"] - df["close_shift"]) / df["close_shift"] # 2. 取最近一日的全市场快照 latest = df[df["date"] == df["date"].max()].dropna(subset=["pct_change"]).copy() # 3. 横向:全市场排百分位(核心) latest["rps"] = latest["pct_change"].rank(pct=True) * 100 # 4. 突破确认:120 日滚动最高价 roll_high = df.groupby("symbol")["high"].rolling(rps_period, min_periods=rps_period // 2).max().reset_index(level=0, drop=True) df["roll_high"] = roll_high latest_high = df[df["date"] == df["date"].max()][["symbol", "roll_high"]] latest = latest.merge(latest_high, on="symbol") latest["near_high"] = latest["close"] >= latest["roll_high"] * 0.9 return latest def make_market(n_stocks: int = 200, n_days: int = 130) -> pd.DataFrame: """造一个 200 只股票的小型市场,120 日内涨幅有强有弱。""" np.random.seed(42) symbols = [f"{i:06d}" for i in range(1, n_stocks + 1)] dates = pd.date_range("2024-01-01", periods=n_days) # 每只股票有「不同强度」的随机涨幅 rows = [] for i, sym in enumerate(symbols): # 用 i 决定涨幅强度:i 越大涨幅越大 total_return = 0.1 + (i / n_stocks) * 1.5 # 0.1 ~ 1.6(即 10% ~ 160%) trend = (1 + total_return) ** (1 / n_days) - 1 close = 10.0 * (1 + trend) ** np.arange(n_days) close = close * (1 + np.random.normal(0, 0.01, n_days)) # 加点噪音 high = close * 1.005 for d, c, h in zip(dates, close, high): rows.append({"symbol": sym, "date": d, "close": c, "high": h}) return pd.DataFrame(rows) if __name__ == "__main__": df = make_market() result = calc_rps(df) # 全市场 RPS 分布 print("=" * 50) print("全市场 RPS 分布(前 5 + 末 5)") print("=" * 50) sorted_result = result.sort_values("rps", ascending=False) print("【涨幅跑赢市场 90%+ 的标的】") print(sorted_result.head(5)[["symbol", "pct_change", "rps", "near_high"]]) print("\n【涨幅跑赢市场 10% 不到的标的】") print(sorted_result.tail(5)[["symbol", "pct_change", "rps", "near_high"]]) # RPS ≥ 90 且接近阶段高点的「**真强者**」 print("\n" + "=" * 50) print("最终入选:RPS ≥ 90 且接近阶段高点") print("=" * 50) final = result[(result["rps"] >= 90) & result["near_high"]] print(f"共 {len(final)} 只:") print(final[["symbol", "pct_change", "rps"]].sort_values("rps", ascending=False))
预期输出:
================================================== 全市场 RPS 分布(前 5 + 末 5) ================================================== 【涨幅跑赢市场 90%+ 的标的】 symbol pct_change rps near_high 0 000190 0.5547 100.0000 True 0 000189 0.5438 99.5025 True 0 000188 0.5329 99.0050 True 0 000187 0.5220 98.5075 True 0 000186 0.5111 98.0100 True 【涨幅跑赢市场 10% 不到的标的】 symbol pct_change rps near_high 0 000004 0.1132 10.4478 True 0 000003 0.1109 10.0000 True 0 000002 0.1086 9.5025 True 0 000001 0.1063 9.0050 True ... ================================================== 最终入选:RPS ≥ 90 且接近阶段高点 ================================================== 共 19 只: symbol pct_change rps 0 000190 0.5547 100.0000 0 000189 0.5438 99.5025 ...
RPS 把「绝对涨幅」变成「相对位置」——强者被识别,弱者被过滤。
| 维度 | 欧奈尔原版 | Sequoia-X 改良版 |
|---|---|---|
| RPS 周期 | 250 日(约一年) | 120 日(约半年) |
| 阈值 | RPS ≥ 80 | RPS ≥ 90(更严格) |
| 突破确认 | 不强调 | 收盘 ≥ 120 日最高 × 0.9 |
| 取样方式 | 通常按周取样 | 按日取样(更及时) |
Sequoia-X 用「更短周期 + 更严阈值 + 加突破确认」——信号数量更少但质量更高。
RPS 的独特价值在于它打破了「单只股票视角」的局限:
| 视角 | 局限 | RPS 突破 |
|---|---|---|
| 单只股票看均线 | 不知道它在全市场的位置 | RPS 告诉你「跑赢 90%」 |
| 看绝对涨幅 | 涨 30% 不知强还是弱 | RPS 告诉你「强」 |
| 看技术形态 | 单股形态孤立 | RPS 告诉你「全市场最强」 |
这正是「RPS 是技术面 + 基本面的桥梁」——它用「涨幅」这个技术指标,间接反映了「业绩 / 题材 / 资金」这些基本面因素。
| 市况 | RPS 信号表现 |
|---|---|
| 牛市/反弹市 | 最有效——强者恒强 |
| 震荡市 | 强者不恒强——RPS 容易失效 |
| 熊市 | 几乎全市场 RPS 向下——RPS 失效 |
| 政策市 | 短期 RPS 剧烈波动——单日排名可能反转 |
RPS 是「全市场横向视野」的策略,必须有「全市场一起涨」的环境——熊市 / 政策市 单独使用风险大。
RPS 相对强度 ✅ ← 本节 │ ▼ 下一章(第 7 章 事件驱动策略与消息推送):另一类非行情信号 + 飞书卡片构建
rank(pct=True) * 100:pandas 横向排位的灵魂,一行代码把绝对值变相对位置。配套教学脚本:
images/rps_demo.py——可用任意合成数据验证 RPS 计算。
至此,六大技术面策略全部精读完毕。下一章进入事件驱动策略(定增公告)与消息推送(飞书卡片)。