import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import yfinance as yf
data = yf.download('AAPL', start='2022-01-01', end='2024-01-01', progress=False)
close = data['Close'].squeeze()
high = data['High'].squeeze()
low = data['Low'].squeeze()
import matplotlib.pyplot as plt
# 1. 设置系统自带的中文字体(这里使用黑体 SimHei)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 如果你想用微软雅黑,可以改成 ['Microsoft YaHei']
# 2. 解决更换字体后,负号(-)显示为方块的问题
plt.rcParams['axes.unicode_minus'] = False
1. RSI — 相对强弱指数¶
为什么需要 RSI?¶
股价连涨 10 天之后,你心里可能会想:「这还能继续涨吗?是不是快涨到头了?」
RSI 就是回答这个问题的工具。 它的核心逻辑是:
在过去 N 天里,上涨的力气 和 下跌的力气 谁更强?
- 如果上涨力气远大于下跌力气 → RSI 接近 100 → 「买得太猛了,超买了,可能要回调」
- 如果下跌力气远大于上涨力气 → RSI 接近 0 → 「跌过头了,超卖了,可能要反弹」
- RSI 在 30~70 之间 → 市场力量比较均衡,中性区间
公式¶
先计算过去 14 天里,每天涨幅的平均(RS 的分子)和每天跌幅的平均(RS 的分母):
$$RS = \frac{\text{14日平均上涨幅度}}{\text{14日平均下跌幅度}}$$
然后把这个比值压缩到 0~100 的区间:
$$RSI = 100 - \frac{100}{1 + RS}$$
这个公式只是一个数学压缩手段,核心还是那个比值 RS。
使用规则(参考,不是定律)¶
- $RSI > 70$:超买区域(可能回调,但强势行情可以维持很久)
- $RSI < 30$:超卖区域(可能反弹,但弱势行情可以一直跌)
- 不能单独使用! RSI 是辅助工具,要结合趋势判断使用
def compute_rsi(price, period=14):
delta = price.diff() # 每日涨跌变化
gain = delta.clip(lower=0) # 只保留上涨部分(跌的那天为 0)
loss = -delta.clip(upper=0) # 只保留下跌部分(涨的那天为 0)
avg_gain = gain.ewm(com=period - 1, min_periods=period).mean() # 平均涨幅
avg_loss = loss.ewm(com=period - 1, min_periods=period).mean() # 平均跌幅
rs = avg_gain / avg_loss # 上涨力气 vs 下跌力气的比值
rsi = 100 - (100 / (1 + rs)) # 压缩到 0~100
return rsi
rsi = compute_rsi(close)
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(13, 7), sharex=True)
ax1.plot(close.index, close.values, linewidth=1.2, color='steelblue')
ax1.set_title('AAPL 收盘价', fontsize=12)
ax1.set_ylabel('价格')
ax1.grid(alpha=0.3)
ax2.plot(rsi.index, rsi.values, linewidth=1.2, color='purple')
ax2.axhline(70, color='red', linestyle='--', alpha=0.7, label='超买线 (70)')
ax2.axhline(30, color='green', linestyle='--', alpha=0.7, label='超卖线 (30)')
ax2.axhline(50, color='gray', linestyle='-', alpha=0.3)
ax2.fill_between(rsi.index, 70, rsi.values, where=(rsi.values > 70), alpha=0.2, color='red', label='超买区域')
ax2.fill_between(rsi.index, 30, rsi.values, where=(rsi.values < 30), alpha=0.2, color='green', label='超卖区域')
ax2.set_ylim(0, 100)
ax2.set_title('RSI (14) — 越高代表近期上涨力气越强', fontsize=12)
ax2.set_ylabel('RSI')
ax2.legend(loc='upper left')
ax2.grid(alpha=0.3)
plt.tight_layout()
plt.show()
2. Stochastic Oscillator (KDJ)¶
为什么需要 KDJ?¶
RSI 看的是「涨跌力气的比较」,而 KDJ 看的是另一个角度:
今天的收盘价,在过去 N 天的最高最低价范围内,处于什么位置?
举个例子:过去 9 天,苹果的最低价是 150 美元,最高价是 180 美元。
- 如果今天收盘在 179 美元 → 非常接近高点 → K 值接近 100 → 可能超买
- 如果今天收盘在 151 美元 → 非常接近低点 → K 值接近 0 → 可能超卖
这个「相对位置」的概念,叫做 RSV(Raw Stochastic Value,原始随机值)。
三条线的含义¶
$$K = \frac{P_{close} - L_{n}}{H_{n} - L_{n}} \times 100 \quad \text{(当前价格相对于区间的位置,0~100)}$$
然后对 K 再做一次平滑,得到更稳定的 D;对 D 再放大,得到最灵敏的 J:
$$D = EMA_3(K) \quad \text{(K 的均线,更平滑)}$$ $$J = 3K - 2D \quad \text{(放大器:超出 0~100 范围,更早发出预警)}$$
使用要点¶
- K 和 D 形成金叉/死叉 → 买入/卖出信号
- J 线超过 100 → 极度超买;J 线低于 0 → 极度超卖(J 线最敏感,也最容易出假信号)
def compute_kdj(high, low, close, n=9, m=3):
lowest_low = low.rolling(n).min() # 过去 9 天最低价
highest_high = high.rolling(n).max() # 过去 9 天最高价
rsv = (close - lowest_low) / (highest_high - lowest_low) * 100 # 相对位置 0~100
K = rsv.ewm(com=m - 1, adjust=False).mean()
D = K.ewm(com=m - 1, adjust=False).mean()
J = 3 * K - 2 * D
return K, D, J
K, D, J = compute_kdj(high, low, close)
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(13, 7), sharex=True)
ax1.plot(close.index, close.values, linewidth=1.2, color='steelblue')
ax1.set_title('AAPL 收盘价', fontsize=12)
ax1.grid(alpha=0.3)
ax2.plot(K.index, K.values, label='K(相对位置)', linewidth=1.2, color='blue')
ax2.plot(D.index, D.values, label='D(K 的均线)', linewidth=1.2, color='orange')
ax2.plot(J.index, J.values, label='J(放大器,最敏感)', linewidth=1, color='green', alpha=0.7)
ax2.axhline(80, color='red', linestyle='--', alpha=0.5, label='超买 (80)')
ax2.axhline(20, color='green', linestyle='--', alpha=0.5, label='超卖 (20)')
ax2.set_ylim(-10, 110)
ax2.set_title('KDJ 指标 (9, 3, 3)', fontsize=12)
ax2.legend(loc='upper left', ncol=2)
ax2.grid(alpha=0.3)
plt.tight_layout()
plt.show()
3. 动量指标 (Momentum)¶
为什么需要它?¶
有一个非常简单的市场观察:涨势中的股票,往往继续涨;跌势中的股票,往往继续跌。 这种现象叫做「动量效应」(Momentum Effect),是被学术界和实践者广泛验证的规律之一。
动量指标就是直接量化这个现象的:
今天的价格,比 N 天前涨了多少?
- 动量为正 → 今天比 N 天前贵,说明最近 N 天整体在涨
- 动量为负 → 今天比 N 天前便宜,说明最近 N 天整体在跌
- 动量从负转正 → 可能是趋势的转折点
公式¶
$$Mom_n(t) = \frac{P_t}{P_{t-n}} - 1 \quad \text{(今天比 N 天前涨了多少百分比)}$$
这个就是我们熟悉的 pct_change(n),超级简单。
为什么用两个不同周期?¶
- 短期动量(20日):反应灵敏,抓短期趋势,但噪声多
- 长期动量(60日):反应迟钝,但趋势信号更可靠
- 两者同向 → 趋势确立;反向 → 可能是短期波动或趋势转折
mom_20 = close.pct_change(20) # 20日动量(今天 vs 20天前)
mom_60 = close.pct_change(60) # 60日动量(今天 vs 60天前)
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(13, 7), sharex=True)
ax1.plot(close.index, close.values, linewidth=1.2, color='steelblue')
ax1.set_title('AAPL 收盘价', fontsize=12)
ax1.grid(alpha=0.3)
ax2.plot(mom_20.index, mom_20.values, label='20日动量(短期)', linewidth=1.2, color='blue')
ax2.plot(mom_60.index, mom_60.values, label='60日动量(长期)', linewidth=1.5, color='red', alpha=0.7)
ax2.axhline(0, color='black', linewidth=0.8)
ax2.fill_between(mom_20.index, 0, mom_20.values,
where=(mom_20.values >= 0), alpha=0.15, color='green', label='短期正动量')
ax2.fill_between(mom_20.index, 0, mom_20.values,
where=(mom_20.values < 0), alpha=0.15, color='red', label='短期负动量')
ax2.yaxis.set_major_formatter(plt.FuncFormatter(lambda y, _: f'{y:.0%}'))
ax2.set_title('价格动量 — 0轴以上代表近期上涨趋势', fontsize=12)
ax2.legend()
ax2.grid(alpha=0.3)
plt.tight_layout()
plt.show()
🎯 练习¶
- RSI 的超买/超卖阈值通常是 70/30,试改为 80/20,比较信号数量差异。(阈值越严格,信号越少,但每个信号的可靠性通常更高)
- 在某段强势上涨行情中,RSI 是否长期处于高位?这说明了什么?(强趋势中 RSI 超买不等于马上会跌)
- 实现规则:RSI < 30 时买入,RSI > 70 时卖出,计算其历史表现。
下一节 → 03_volatility_indicators.ipynb