1. 这节究竟解决什么实际问题?¶
3.2 动量类指标 的具体任务是:比较动量、RSI、KDJ 等读数,理解它们描述速度而非保证反转。 本节将明确输入数据、计算过程、输出及其在后续研究中的用途。
2. 前置知识¶
理解收益率、滚动窗口和基础 DataFrame 操作。 对公式的理解应包括变量定义、单位、时间索引和适用条件。
3. 数据说明¶
使用固定日频价格样本;高低开收和成交量如为教学代理,会在代码中明确说明。 运行前先检查 shape、日期范围、列名与缺失值。数据看起来能画图,不代表它适合回答研究问题。
4. 学习目标与完成标准¶
完成本节后,你应能用自己的话解释 3.2 动量类指标 的输入、计算步骤、输出和局限;尤其能说明:比较动量、RSI、KDJ 等读数,理解它们描述速度而非保证反转。 你还应能独立改动一个参数并解释结果为什么变化,指出至少一个会让结论失真的假设。
5. 核心概念与公式¶
技术指标将历史价格或成交量压缩为统计量。指标本身不包含交易方向、执行时点和风险约束。
滚动指标只使用窗口结束日及以前的数据;交易仓位必须至少滞后一个可交易周期。 看到公式时依次确认:每个符号的单位是什么?它属于哪个时点?分母会不会为零?它是描述性统计量、预测量,还是交易规则?
6. 从零实现¶
原始代码按照“准备输入 → 计算中间量 → 得到结果 → 解释输出”的顺序组织。每个单元应检查变量类型、索引、维度和代表性数值。中间变量用于核对公式实现和定位计算错误。
7. 成熟库实现¶
公共指标函数减少重复代码;首次学习仍应读懂每个滚动、平滑和标准化步骤。 使用库函数前先阅读参数含义、默认值和返回对象;库输出一个数字,并不说明这个数字适合你的交易假设。
8. 两种实现的差异¶
从零实现便于核对公式、调试和识别隐含假设;成熟库适合在计算逻辑已验证后复用。两种实现依赖相同的数据口径与时点假设,使用库函数不会改变输入数据的质量。
9. 预期结果与检查范围¶
你应看到维度正确的 Series/DataFrame、日期单调递增的索引,以及能支持“比较动量、RSI、KDJ 等读数,理解它们描述速度而非保证反转。”这一任务的表格或图形。金融日收益率均值通常接近 0;若出现极大的连续收益、空表、全 NaN 或索引错位,应先排查数据和对齐逻辑,而不是立即相信结果。
10. 常见错误与诊断¶
窗口不足时误读 NaN、同日用收盘信号按收盘成交、忽略除零、把指标阈值当普适规律。
诊断顺序:先打印类型与 shape,再看 head()/tail(),接着检查日期和 NaN,最后才比较数值。这样能避免在错误的对象上反复改参数。
11. 研究偏差检查¶
测试大量窗口后只保留样本内表现最高的参数会产生数据窥探偏差;任何信号都应和简单基线比较。 结果至少应与一个简单基线比较,并明确哪些部分只是教学演示。
12. 三层练习与答案¶
- 基础练习:验证一个中间变量、补全一段小计算,并用一句话解释输出。
- 研究练习:改变窗口、样本区间、约束或标的,记录哪些结论仍成立。
- 开放项目:把本节方法嵌入模块项目,报告基线、失败结果和限制。
题目在 ../exercises/03_indicators/02_momentum_indicators_exercises.ipynb;完整推导、可运行参考实现与结果检查在 ../solutions/03_indicators/02_momentum_indicators_solutions.ipynb。请先独立作答,再打开答案。
13. 延伸阅读¶
延伸材料:Technical Analysis of the Financial Markets(Murphy)。阅读记录应包括研究假设、数据频率、样本范围和结论适用条件。
14. 下一节¶
下一节是 03_volatility_indicators。本节的概念会成为理解它的输入,而不是孤立的名词。
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()
# 默认使用固定样本,保证课程与 CI 不依赖实时接口。
USE_OFFLINE_DATA = True
if USE_OFFLINE_DATA:
from pathlib import Path
import sys
docs_root = next(parent for parent in [Path.cwd(), *Path.cwd().parents]
if (parent / 'utils' / 'offline_data.py').exists())
sys.path.insert(0, str(docs_root))
from utils.offline_data import configure_yfinance_offline
configure_yfinance_offline(yf)
else:
print('在线下载为扩展步骤;请核对数据字段、时区和下载日期。')
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