Insights·2026-08-07

RSI、MACD 与布林带 — 量化智能体直接调用的五个技术指标

技术指标是从开、高、低、收、量这一份原始数据再计算一层得到的二阶数值。指标有数百种,但 AI 交易智能体通过单个工具直接调用的恰好是五个:RSI(14)、MACD(12/26/9)、布林带(20, 2σ)、SMA(20/50/200) 和 EMA(20),它们分别覆盖超买超卖、趋势反转、波动率与长期趋势。问题在于同一个名字并不代表同一个数字。RSI 取决于平滑方式是 Wilder 法(ewm alpha=1/14) 还是简单移动平均(rolling 14);布林带取决于标准差用 ddof=1 还是 ddof=0。接入指标之前该确认的不是阈值,而是计算公式。

보조지표 다섯(RSI 14, MACD 12/26/9, 볼린저 20·2σ, SMA·EMA)의 역할을 정리하고 rolling 단순이동평균과 Wilder ewm 두 계산 방식을 나란히 비교한 요약 도식
에이전트가 툴 하나로 부르는 보조지표 다섯과, 같은 이름이 다른 값을 내는 두 계산 방식

技术指标到底是什么

打开任何图表,蜡烛图上下都铺着若干条线,那就是技术指标 —— 由价格与成交量再计算一层得到的二阶数值。

原始数据只有一份。每根K线包含开盘、最高、最低、收盘和成交量,合称 OHLCV。所有指标都源自它。它们并不增加信息,只是把肉眼从蜡烛图上读不出的性质变成数字。

这类性质大致有四种:是否涨过头或跌过头(过热)、方向是否正在改变(趋势反转)、波动有多剧烈(波动率)、大方向朝哪边(长期趋势)。指标有数百种,但多数只是用不同方式测量这四者之一。

所以一开始不需要看几十个指标,把这四类各覆盖一个就够了。

智能体直接调用的恰好是五个

取一个具体参照。香港大学数据智能实验室(HKUDS)开源的交易智能体 Vibe-Trading,可以用自然语言下指令,自动取数据并跑回测。打开这个仓库会发现,智能体通过单个工具即时调用的指标正好五个。

它们以常量写在 agent/src/tools/technical_indicator_tool.py 里:RSI 为 14,MACD 为 12/26/9,布林带为 20 日加减两倍标准差,SMA 为 20/50/200,EMA 为 20。括号里的数字是计算窗口长度,也就是回看多少根K线。

这五个正好对上前面四类性质。RSI 管过热,MACD 管反转,布林带管波动率,SMA 与 EMA 管趋势。图表界面另外多了一个 KDJ(9),但面向智能体的工具里没有。

为什么只有五个?因为都出自同一份 OHLCV,指标之间高度重叠。随机指标与 RSI 测的其实是同一件事,叠在一起只会增加信心而不增加信息。停在五个应当读作取舍,而不是功能缺失。

指标默认参数测量对象取值范围
RSI14超买 / 超卖0 – 100
MACD12 / 26 / 9趋势反转时点价格单位(无上下限)
布林带20, 2σ波动率通道价格单位(上/中/下轨)
SMA20 / 50 / 200趋势线价格单位
EMA20近期加权趋势线价格单位

RSI(14) — 把过热压缩到 0 到 100 之间

RSI 即相对强弱指数。它比较最近 14 根K线中上涨日与下跌日的力量,压缩成一个数字。

计算分四步。第一,取相对前一日的变化量。第二,把变化量拆成上涨日的涨幅与下跌日的跌幅两支。第三,各自按 14 日取平均。第四,用平均涨幅除以平均跌幅得到 RS,代入 RSI = 100 - 100 / (1 + RS)。

公式的形状把取值锁在 0 与 100 之间。若 14 日连涨,平均跌幅为 0,RSI 就是 100;反之为 0。实际大多在 30 到 70 之间摆动。

读法是惯例。70 以上称超买,30 以下称超卖。这两个数字并非从公式推出,而是创立者 J. Welles Wilder 定下的,因此通常按标的与市场调整。

有一个陷阱。强势上涨中 RSI 会在 70 上方停留数周。一旦越过 70 就卖出的规则,会整段错过行情的起步阶段。RSI 报告的是强度而非方向,必须与趋势类指标配合看。

MACD(12/26/9) — 用两条均线的距离捕捉反转

MACD 即指数平滑异同移动平均。名字长,做的事很简单:测量快均线与慢均线之间的距离。

它产出三个值。MACD 线是 12 日 EMA 减 26 日 EMA,短均线在长均线之上为正,反之为负。信号线是把 MACD 线再用 9 日 EMA 平滑一次。柱状图是 MACD 线减信号线,画成柱子让两线间距可见。

读的是交叉。MACD 线自下向上穿过信号线是金叉,视为偏多信号;自上向下穿过是死叉。柱状图会比交叉略早趋近于零,因此可用作提前提示。

MACD 线穿越零轴要单独看。上穿零轴意味着 12 日均线已超越 26 日均线,趋势本身发生了翻转。

陷阱是滞后。移动平均是已发生价格的平均,因此总在真正的转折之后才反应。在剧烈波动的标的上信号来得晚,在横盘区间里交叉会无意义地反复。缩短窗口会更快,噪音也同样增加。

布林带(20, 2σ) — 用波动率画出通道

布林带由三条线组成。中轨是 20 日简单移动平均,上轨在其上加两倍 20 日标准差,下轨则减去。价格看起来像在这条通道中穿行。

关键在于通道宽度不是固定的。标准差就是波动率的大小,价格剧烈震荡时带口张开,安静时收窄。也就是说这个指标真正展示的是波动率,而不是价格位置。

因此实战中最常盯的是收口。带宽相对近期异常收窄,说明波动率已死,之后通常朝某一方向大幅移动。它预告幅度,不预告方向。

两倍标准差这个数字也有依据。若数值服从正态分布,均值 ± 2 倍标准差内约覆盖 95%。但真实价格变动并非正态,尾部更厚,因此突破带口的情况远比理论频繁。

所以触碰带口本身不是信号。碰到上轨就卖出的规则,在强势上涨中会持续出错,沿着上轨爬升的现象甚至有专门的名字叫带口行走。布林带更适合用来判断波动率状态,而非价格位置。

SMA(20/50/200) 与 EMA(20) — 最古老也最常用的趋势线

SMA 是简单移动平均:把最近 N 日收盘价相加再除以 N。20 日线约为一个月,50 日线约为一个季度,200 日线大致相当于一年的交易日。

200 日线受到特殊对待,原因是惯例而非数学。足够多的机构把它当作长期趋势基准,因此价格在 200 日线之上还是之下,本身就会影响市场参与者的行为。

EMA 是指数移动平均。SMA 把 20 天等权对待,EMA 则给近期价格更大权重,久远价格的影响按指数衰减。它转向比 SMA 快,对噪音的反应也同样快。

把两条线叠加就得到金叉与死叉。短均线上穿长均线为金叉,下穿为死叉。常用的组合是 50 日线与 200 日线。

陷阱与 MACD 相同。均线回看过去,必然滞后。金叉常常出现在涨势已推进相当一段之后,而在横盘区间两线纠缠,信号会接连反转。

用 pandas 亲手算出全部五个

不必额外安装库,单靠 pandas 就能算出全部五个。输入只有收盘价一项,五个函数每个都在十行以内。

把下面的代码保存为 indicators.py,传入收盘价的 pandas Series 即可。收盘价可以用 yfinance、pykrx 取得,也可以用 pandas 读取券商导出的 CSV。

ewm 的 adjust=False 很重要。默认的 True 会用另一套公式修正初始区间,漏掉这个参数会让前面几十根K线的 EMA 与其他工具对不上。

函数写好后建议校验一次。把同一标的在券商 App 或 TradingView 上打开,比较同一日期的 RSI。数字可能对不上 —— 原因就是下一节的内容。

indicators.py
import pandas as pd


def rsi(close: pd.Series, period: int = 14) -> pd.Series:
    delta = close.diff()
    gain = delta.clip(lower=0)
    loss = (-delta).clip(lower=0)
    avg_gain = gain.ewm(alpha=1 / period, adjust=False).mean()
    avg_loss = loss.ewm(alpha=1 / period, adjust=False).mean()
    return 100 - 100 / (1 + avg_gain / avg_loss)


def macd(close, fast=12, slow=26, signal=9):
    line = (close.ewm(span=fast, adjust=False).mean()
            - close.ewm(span=slow, adjust=False).mean())
    sig = line.ewm(span=signal, adjust=False).mean()
    return pd.DataFrame({"macd": line, "signal": sig, "hist": line - sig})


def bollinger(close, period=20, num_std=2.0, ddof=0):
    mid = close.rolling(period).mean()
    sd = close.rolling(period).std(ddof=ddof)
    return pd.DataFrame({"upper": mid + num_std * sd,
                         "middle": mid,
                         "lower": mid - num_std * sd})


def sma(close, period):
    return close.rolling(period).mean()


def ema(close, period):
    return close.ewm(span=period, adjust=False).mean()
使用
import yfinance as yf
from indicators import rsi, macd, bollinger, sma, ema

close = yf.download("AAPL", period="1y")["Close"].squeeze()

print("RSI(14) :", round(rsi(close).iloc[-1], 2))
print("MACD    :", macd(close).iloc[-1].round(4).to_dict())
print("BB(20,2):", bollinger(close).iloc[-1].round(2).to_dict())
print("SMA200  :", round(sma(close, 200).iloc[-1], 2))
print("EMA20   :", round(ema(close, 20).iloc[-1], 2))

同名不同数 — RSI 的两种平滑方式

这是本文最要紧的一段。同样写着 RSI 14,不同工具算出的值可能不一样,因为第三步——对涨幅与跌幅取平均——有两种做法。

一种是 Wilder 法,创立者最初提出的方法,用指数加权移动平均并取 alpha = 1 / 14。过去的值不会彻底消失,会持续留下一点影响。TA-Lib 就是这一种 —— 其源码 ta_RSI.c 的注释写着 "Wilder's approach",把前一个均值乘以 period-1,加上今日值,再除以 period(2026-08-07 查阅源码)。

另一种是简单移动平均:只把最近 14 日等权平均,第 15 日之前的值直接丢弃。它被当作独立指标,甚至有自己的名字叫 Cutler's RSI。数值比 Wilder 法更敏感,且当大值滑出窗口时可能突然跳变。

这不是假设。在前面提到的 Vibe-Trading 仓库中,technical_indicator_tool.py 里的 RSI 函数用 rolling(14).mean() 计算。而同一仓库的策略技能文档 technical-basic/SKILL.md 明确写着 RSI 与 ADX 使用 Wilder EWM,即 ewm(alpha=1/period)。两种做法共存于同一个仓库(2026-08-07 查阅源码)。

这不是谁对谁错,两者都是实际在用的定义。问题在于不知情地在一边回测、在另一边实盘:规则相同,进场点却不同。若条件是 RSI 突破 70,那么 69.4 与 71.2 就是两笔不同的交易。

并排计算两种做法看差异
wilder = gain.ewm(alpha=1 / 14, adjust=False).mean()   # Wilder 원본 · TA-Lib ta_RSI.c
cutler = gain.rolling(14).mean()                       # Cutler's RSI · 简单移动平均

# 用同一份收盘价构造两个 RSI 并比较最后一个值
print(round(rsi_wilder(close).iloc[-1], 2),
      round(rsi_cutler(close).iloc[-1], 2))

布林带同样如此 — 标准差的自由度 ddof

计算标准差时,离差平方和除以什么也有两种做法。除以样本数 n 得到总体标准差(ddof=0),除以 n - 1 得到样本标准差(ddof=1)。统计学中用样本推断总体时惯例使用 n - 1。

pandas 的 rolling(20).std() 默认 ddof=1。不传任何参数得到的就是样本标准差。

TA-Lib 不同。查看源码 ta_BBANDS.c,带宽计算最终落到 ta_VAR.c 的方差公式,而该公式是 variance = sum(x²)/n - mean²。除以 n 即总体方差,因此 ddof=0(2026-08-07 查阅源码)。

差距可以算出来。样本标准差等于总体标准差乘以 sqrt(n / (n - 1))。n 为 20 时 sqrt(20 / 19) = 1.0260,因此用 pandas 默认值画出的带口,半宽比 TA-Lib 宽 2.60%。

2.60% 看着不大,但一旦把触碰带口作为交易条件就不同了。价格贴近带口的那些日子,一边会触发而另一边不会。这正是回测与实盘对不上时,先看这里再怀疑策略的理由。

对齐方法很简单。用哪一种都可以,但回测与实盘必须一致,因此在代码里显式写出 ddof。上一节 bollinger 函数把 ddof=0 设为默认值就是这个意思。

直观确认 ddof 的差异
sd_sample = close.rolling(20).std()          # pandas 默认值 · ddof=1
sd_pop = close.rolling(20).std(ddof=0)       # 与 TA-Lib 一致的总体标准差

print(round(sd_sample.iloc[-1] / sd_pop.iloc[-1], 4))   # 1.026 = sqrt(20 / 19)

接入指标之前的检查清单

第一,写明窗口长度。RSI 14、MACD 12/26/9、布林带 20 都只是默认值,各工具可能不同。在代码里写成参数,日后数字对不上时能迅速缩小排查范围。

第二,确认平滑方式。RSI、ADX 这类 Wilder 创立的指标,原始定义是 ewm(alpha=1/period)。不论库文档怎么写,直接看源码是 rolling 还是 ewm,一分钟就够。

第三,写明 ddof。所有基于标准差的指标都适用,不只是布林带,把波动率本身当信号时同样如此。

第四,确认 EMA 的 adjust。pandas ewm 默认 adjust=True 会用不同方式计算初始区间。要与其他工具对齐就用 adjust=False。

第五,不要只凭单个指标建立交易规则。前面提到的 Vibe-Trading 的 technical-basic 技能,会分别计算趋势(EMA 与 ADX)、均值回归(布林带与 RSI)、量价(OBV 与量比)三个维度,再投票合成一个信号:三者同向才进场,出现分歧就观望。

总结一句:学技术指标,一半是知道每个指标测什么,另一半是知道自己用的工具怎么算它。跳过后一半,你会得到数字,却不知道数字是什么意思。