第 4 章 · 03 RSI:头肩顶形态识别 本节摘要:本节精读 RSI 策略——但作者对 RSI 这个指标本身毫不客气,开篇就表白"i believe its kinda bull shit(我其实觉得它有点扯)"。本节提供两套玩法:① 最朴素的 30/70 超买超卖阈值;② 在 RSI 序列上直接做头肩顶(head-shoulder)形态识别,找 7 个节点 m-n-l-j-k-o-i。我们重点讲清楚 递归实现的 Wilder 原版 smoothed moving average、 的 7 节点查找,以及那个让作者果断放弃背离策略的公案——发明人 Wilder 与其门徒 Cardwell 在"熊背离"上互相矛盾。
本节摘要:本节精读 RSI 策略——但作者对 RSI 这个指标本身毫不客气,开篇就表白"i believe its kinda bull shit(我其实觉得它有点扯)"。本节提供两套玩法:① 最朴素的 30/70 超买超卖阈值;② 在 RSI 序列上直接做头肩顶(head-shoulder)形态识别,找 7 个节点 m-n-l-j-k-o-i。我们重点讲清楚
smma递归实现的 Wilder 原版 smoothed moving average、pattern_recognition的 7 节点查找,以及那个让作者果断放弃背离策略的公案——发明人 Wilder 与其门徒 Cardwell 在"熊背离"上互相矛盾。还有个作者的小脾气:他故意把"head and shoulders"简写成"head-shoulder",因为**"拒绝给洗发水 Head&Shoulders 免费广告,而且那洗发水根本没用"**。
涉及源码:原项目
RSI Pattern Recognition backtest.py(约 390 行),数据 yfinance 拉取 FCAU(菲亚特克莱斯勒)2016-01-01 至 2018-01-01 日线。
⚠️ 注意:本节策略的形态识别是直接在 RSI 上做,不是在价格上。看代码前务必记住这一点,否则会把
df['Close']的比较误以为是"价格头肩",其实它只是用来定位节点坐标,真正的形态判据落在 RSI 曲线上。
阅读完本节,你应当能够:
smma 递归 smoothed moving average(Wilder 原版)的公式。pattern_recognition 的 7 节点查找:m-n-l-j-k-o-i 各自的定位与条件。RSI(Relative Strength Index,相对强弱指数)反映价格动量的强弱。作者推荐的资料是 TradingView Wiki——"its not like i work there and try to promote it, trading view wiki is a very detailed encyclopedia"。
计算逻辑(以默认 14 日为例):
delta>0,否则 0)和"向下"(delta<0 取绝对值,否则 0)两组。RSI 恒在 0 到 100 之间。RS 越大(涨得多跌得少),RSI 越接近 100;RS 越小,RSI 越接近 0。
本节用的是 smoothed moving average(SMMA),作者特别标注"the authentic way(正宗做法)"。smma 函数(L43-51)是一个递归:
def smma(series, n): output = [series[0]] for i in range(1, len(series)): temp = output[-1] * (n-1) + series[i] output.append(temp / n) return output
写成递推公式:
SMMA_t = (SMMA_{t-1} * (n-1) + series_t) / n
这等价于一个 α=1/n 的指数加权移动平均(EMA),但种子值用 series[0] 而非前 n 项的 SMA。它是 RSI 发明人 J. Welles Wilder Jr. 在 1978 年《New Concepts in Technical Trading Systems》里用的原始平滑法,所以叫"正宗"。相比简单移动平均,SMMA 对近端数据更敏感、对远端数据缓慢衰减,更贴合"动量"的语义。
TradingView Wiki 列了 RSI 的几种玩法:
signal_generation 就是这个。关于背离,有一段让作者彻底放弃它的公案:
"the inventor of rsi called wilder believes bearish rsi divergence creates a selling opportunity. but his protege cardwell believes bearish divergence only occurs in a bullish trend. so their ideas basically contradict to each other, i would undoubtedly give up on this bs divergence strategy."
发明人 Wilder 认为熊背离是卖出时机;他的门徒 Cardwell 却认为熊背离只在多头趋势中出现。师徒俩直接打架。作者的结论很干脆:"我果断放弃这个扯淡的背离策略。"——这是一个值得记住的方法论:当一个指标的"正确用法"连发明人和嫡系门徒都吵不清时,别用它。
本节的主菜是头肩顶形态识别——但直接在 RSI 上做,不在价格上。头肩顶本来叫"head and shoulders",作者却全程写成"head-shoulder",理由是个冷幽默:
"i refused to do free marketing for the shampoo, cuz that shampoo doesnt work at all!"(我拒绝给那个洗发水做免费广告,因为那洗发水根本没用!)
Head & Shoulders 是宝洁旗下的知名洗发水品牌,与"头肩顶"英文同名。作者的小脾气让本节代码里所有变量名(head、shoulder)都成了"洗发水梗"的载体。
最简单的版本在 signal_generation(L81-90):
def signal_generation(df, method, n=14): df['rsi'] = 0.0 df['rsi'][n:] = method(df['Close'], n=14) df['positions'] = np.select([df['rsi']<30, df['rsi']>70], [1, -1], default=0) df['signals'] = df['positions'].diff() return df[n:]
np.select 是关键:条件列表 [rsi<30, rsi>70] 对应值列表 [1, -1],都不满足则 default=0(空仓)。signals = positions.diff() 把"持仓方向"转成"执行动作"。这套写法极其紧凑,适合阈值类策略。
真正的主角是 pattern_recognition(L151-307)。它直接在 RSI 上找头肩顶,7 个节点 m-n-l-j-k-o-i:
n j o ← 左肩 头 右肩 / \ / \ / \ / \ / \ / \ m / \ l / \ k / \ i ← 左底 左颈底 颈底 右底 起点(右端)
(注:i 是迭代起点,在 RSI 序列右端;m 在最左端;头 j 是最高点;左右肩 n、o 次高;左右底及颈底 m、l、k 与 i 近似等高。)
关键超参:
period = 25 # 形态查找的回溯视野 delta = 0.2 # 两价"近似相等"的阈值 head = 1.1 # 头节点显著高于其他节点的倍数 shoulder = 1.1 # 肩节点显著高于底、又显著低于头的倍数
外层循环从 i = period+lag 开始,i 是右端起点。查找思路与 Bollinger Bands 的 W 底如出一辙——从 i 往回找,用 moveon 开关链短路:
第 1 步 找头节点 j(L220-226):在 [i-period, i) 区间取 Close 的最大值点作为头 j。要求"头显著高于起点":|Close[j] - Close[i]| > head*delta(即头比 i 高出 1.1×0.2=0.22 以上)。把 Close[i] 记为 bottom(底的参考价)。
第 2 步 找节点 k(L231-236):在 (j, i) 之间找一个价格近似等于 bottom 的点 k(|Close[k]-bottom| < delta)——这是右颈底。
第 3 步 找节点 l(L244-249):从 j 向左,找一个近似等于 bottom 的点 l——这是左颈底。注意方向是从 j 向左,为后续在 l 之左找肩节点 n 留空间。
第 4 步 找节点 m(L255-260):在 [i-period, l) 之间(从左往右)找一个近似等于 bottom 的点 m——这是左底。
第 5 步 找左肩 n(L267-273):在 [m, l) 之间取 Close 最大值点作为左肩 n。要求"肩显著高于底且显著低于头":
if (df['Close'][n] - bottom > shoulder*delta) and \ (df['Close'][j] - df['Close'][n] > shoulder*delta):
即 n 比 bottom 高出 0.22 以上,且 j 比 n 高出 0.22 以上——保证"头>肩>底"的层级。
第 6 步 找右肩 o(L281-289):在 (k, i) 之间找一个价格近似等于左肩 n 的点 o(|Close[o]-top| < delta,top=Close[n])。全部凑齐,在 i 处发 signals=-1(看空入场),并把 7 个节点行号存进 coordinates。
头肩顶是看空形态,入场后需要退出条件。作者的退出逻辑很"懒"(L298-305):
if entry_rsi != 0 and moveon == False: counter += 1 if (df['rsi'][i] - entry_rsi > exit_rsi) or (counter > exit_days): df.at[df.index[i], 'signals'] = 1 # 做多平仓 df['cumsum'] = df['signals'].cumsum() counter = 0 entry_rsi = 0
两个退出条件,任一触发就用 signals=1(做多)平掉空头:
作者很坦白:"this is a lazy way to exit the trade, cuz i dont wanna import indicators from other scripts……exiting trades based on rsi is definitely inefficient and unprofitable(这是懒人退场法,因为我不想从别的脚本引入指标……单靠 RSI 退场肯定低效又不赚钱)。"——他建议实战中引入 MACD 或布林带做退出。这种"把局限写在注释里"的诚实是本仓库一贯的风格。

上图是 RSI 曲线上的头肩顶可视化:黄色 RSI 线、黑色 30/70 超买超卖区间、红色粗折线把 7 个节点 m-n-l-j-k-o-i 连起来,标注了 Shoulder / Head / Shoulder。红色倒三角是看空入场,绿色三角是平仓。

第二张图是简单 30/70 策略的 RSI 振荡器:紫色 RSI 线在 30/70 区间内外游走,破 70 做空、破 30 做多。

第三张图是持仓叠加在价格上,可对照两种策略的入场离场节奏。
🎯 反直觉发现:本节的反直觉不在公式,而在两个"作者脾气"。第一,发明人与门徒互相矛盾时,果断放弃该用法——Wilder 说熊背离是卖点,Cardwell 说熊背离只在多头趋势,作者一句"bs divergence"直接弃用。这是一个朴素却强大的方法论:当一个信号的语义连嫡系都吵不清,它就不该进你的策略。第二,指标可以"半信半疑地用"——作者嘴上说 RSI"kinda bull shit",却仍然为它写了 390 行的形态识别;这种"不迷信但不抛弃"的务实态度,正是量化交易者该有的工具观。
SMMA_t = (SMMA_{t-1}*(n-1) + series_t)/n,种子用 series[0],等价于 α=1/n 的 EMA。np.select 写阈值策略极紧凑:条件列表 + 值列表 + default,一行实现 30/70 双向,比 if-else 链清爽。moveon 开关链:与 Bollinger Bands 的 W 底同构,任一层失败则后续全跳过。|price_a - price_b| < delta 判定两节点等高;head/shoulder multiplier=1.1 是"显著高低"阈值,乘以 delta 得到 0.22 的显著差。df.index.get_loc(df['Close'][...].idxmax()) 把时间戳索引转成数值索引,便于后续 range 循环。exit_rsi=4 或 exit_days=5,作者自陈低效,建议实战引入 MACD/布林带。coordinates 存的是 RSI 序列的节点,但画图时叠加在 RSI 曲线上,别误读。cumsum 防重复入场:cumsum[i]==0 保证空仓才找形态,避免叠加仓位。np.select 一行实现。第 4 章到此结束。我们学了三个策略,但最重要的不是公式,而是 Pair Trading 那句"不存在无风险的统计套利",以及 Bollinger Bands 那句"为什么用机器学习,当算术更快更简单"。下一章,我们将从线性的股票/期货世界,跨入非线性的期权世界——先看 Options Straddle 跨式期权如何从双向不确定中获利,再看 VIX Calculator 如何用黎曼和 + 泰勒展开把连续方差互换离散化。