OBV 能量潮实战:用累计成交量把「主力吸筹与派发」画成一条线
OBV(On-Balance Volume,能量潮)是 Joe Granville 在 1963 年提出的、却常被当成「普通成交量指标」误用的工具。它的真身是一条「把每日成交量按涨跌方向累加」的累计曲线:涨日加量、跌日减量。OBV 不预测点位,它度量的是「资金在悄悄流入还是流出」。本文用纯 numpy 从零实现 OBV 全公式,并把「OBV 上穿其 EMA 信号线做多、下穿平仓」的规则在一段「涨/跌 regime 交替、成交量确认趋势」的合成行情上做无前瞻回测:OBV 策略总收益 +27.7%(年化 8.0%、Sharpe 1.04、最大回撤 −8.9%),同期买入持有仅 +2.9%(Sharpe 0.09、最大回撤 −19.5%)。并诚实拆穿累计偏移/无量纲不可比/噪声日失真/独立使用陷阱/参数脆弱五类真实陷阱(中阶)。
你在行情软件里点开「成交量」副图,常会看到一条和价格走势高度同步、却由红绿柱累加而成的曲线,叫 OBV(On-Balance Volume,能量潮)。很多人把它当成「量能大小表」看个热闹。但如果你追问一句——OBV 那个数到底是怎么累加出来的?为什么它能在价格还没动的时候就「提前」拐头?它和单纯看成交量柱状图有什么区别?——能答上来的,十之一二。
结论先放这:OBV 不是「今天成交了多少手」的绝对量,而是一条带方向的累计曲线——涨的那天把全天成交量加上去,跌的那天把全天成交量减下去,平的那天不动。于是 OBV 的斜率就代表了「资金是在净流入(吸筹)还是净流出(派发)」。它的核心价值不是预测点位,而是把「量在价先」这件事量化成一条可交叉、可背离的信号线。它和 CCI、KDJ 不同:后两者是价格的「摆荡」,OBV 是成交量的「方向累计」——所以它能捕捉到「价没动、量已在流」的先导信号。
在我们的合成行情(一段涨/跌 regime 交替、且成交量与当日波动正相关的 800 个交易日)上:用「OBV 上穿其 20 日 EMA 信号线做多、下穿平仓」的规则做无前瞻回测,OBV 策略 总收益 +27.7%(年化 8.0%、Sharpe 1.04、最大回撤 −8.9%);同期买入持有仅 +2.9%(Sharpe 0.09、最大回撤 −19.5%)。OBV 真正厉害的不是多赚 25 个点,而是把最大回撤砍到不到买入持有的一半——这正是量价确认类指标的核心价值:它在「量能转弱、派发开始」时提前离场,把最狠的下跌段躲掉。

一、从涨跌到累计:OBV 的构造逻辑#
OBV 的计算极简,但逻辑不简单。设第 i 日收盘价为 Close(i)、成交量为 Vol(i):
若 Close(i) > Close(i−1): OBV(i) = OBV(i−1) + Vol(i) # 涨日,量记为「流入」
若 Close(i) < Close(i−1): OBV(i) = OBV(i−1) − Vol(i) # 跌日,量记为「流出」
若 Close(i) = Close(i−1): OBV(i) = OBV(i−1) # 平盘,量不动
OBV(0) = 0 (或任意起点,只影响整条曲线平移)plaintext关键认知:OBV 只关心「方向」,不关心「涨跌幅大小」。今天涨 0.1% 和涨 5%,只要收涨,加的都是「全天成交量」;今天跌 0.1% 和跌 5%,减的也都是全天成交量。这意味着 OBV 把「每一块钱成交量」都当成了等权投票——涨时所有量都算多头票、跌时所有量都算空头票。它的隐含假设是:上涨伴随的成交量代表「真实买盘在推动」,下跌伴随的成交量代表「真实卖盘在出逃」。
于是 OBV 的斜率蕴含信息:OBV 持续走高 = 涨日成交量持续大于跌日成交量 = 资金在净吸筹;OBV 拐头向下 = 跌日放量、买盘枯竭 = 派发开始。价格可能还在高位磨,但 OBV 先拐——这就是「量在价先」的直观体现。
二、从零实现:纯 numpy 复刻公式#
我们用纯 numpy 把上面的累加摊开。注意 np.sign 处理涨跌方向,cumsum 做累计。
import numpy as np
def compute_obv(close, volume):
# 1) 每日方向:涨 +1 / 跌 −1 / 平 0
direction = np.sign(np.diff(close, prepend=close[0]))
# 2) 带符号成交量:涨日加量、跌日减量
signed_vol = direction * volume
# 3) 累计得到 OBV(首日为 0)
obv = np.cumsum(signed_vol)
return obvpython这里有几个工程细节必须说清:
- 平盘日(
Close(i)==Close(i−1)):np.sign(0)=0,当天不加不减。但现实中「精确相等」极少,更多是diff≈0的微涨微跌——用np.sign会把微涨微跌也当 ±1 计入。更稳健的写法是对极小 diff 做阈值过滤(如|diff| < eps视为平),避免噪声把 OBV 随机推拉。 - OBV 是「累计偏移量」:它的值取决于起点(这里从 0 起)。所以 OBV 的绝对值没有意义,只有它的「变化、斜率、与信号线的相对位置」有意义——这一点下文陷阱①会再强调。
- 成交量量级:不同标的成交量差几个数量级(茅台日成交几十亿手 vs 小票几千万)。OBV 绝对值不可直接跨标的比,必须归一化(除以基准量或看相对变化)。
光有 OBV 一条噪声很大的累计曲线不够用,实战里几乎一定配一条信号线(OBV 的 EMA)来做交叉:
def ema(x, span):
a = 2.0 / (span + 1)
out = np.empty_like(x, dtype=float)
out[0] = x[0]
for i in range(1, len(x)):
out[i] = a * x[i] + (1 - a) * out[i - 1]
return out
obv_signal = ema(obv, span=20) # OBV 的 20 日指数平滑 = 信号线python三、交易规则:OBV 与信号线的「交叉」才是信号#
裸看 OBV 斜率容易受单日巨量扰動。成熟用法是 OBV 与其信号线(EMA)的交叉:
- 买入:OBV 上穿信号线——意味着「资金净流入的动能刚转正」,吸筹确认。
- 卖出:OBV 下穿信号线——意味着「资金净流出的动能刚转正」,派发确认。
def gen_signals(obv, obv_signal):
N = len(obv)
long_entry = np.zeros(N, dtype=bool)
long_exit = np.zeros(N, dtype=bool)
pos = 0
for i in range(1, N):
cross_up = (obv[i-1] <= obv_signal[i-1]) and (obv[i] > obv_signal[i])
cross_down = (obv[i-1] >= obv_signal[i-1]) and (obv[i] < obv_signal[i])
if pos == 0 and cross_up:
long_entry[i] = True; pos = 1
elif pos == 1 and cross_down:
long_exit[i] = True; pos = 0
return long_entry, long_exitpython规则里「先判断 pos==0 才允许开仓」是状态机,保证不会既多又空、不会重复开仓。回测里每个信号次日按收盘价执行(signal-on-i / execute-on-i+1),严格避免偷看未来——这是所有量价指标回测的红线。
四、实测结果:OBV 的价值写在「躲过下跌」里#
把规则在「涨/跌 regime 交替 + 成交量确认趋势」的合成行情上跑(800 交易日),结果如下:

| 指标 | OBV 信号线交叉 | 买入持有 |
|---|---|---|
| 总收益 | +27.7% | +2.9% |
| 年化 | 8.0% | 0.9% |
| Sharpe | 1.04 | 0.09 |
| 最大回撤 | −8.9% | −19.5% |
| 交易次数 | 51 | 1 |
这张表要诚实读:本段合成行情是「涨/跌 regime 交替」,买入持有几乎原地踏步(+2.9%、Sharpe 仅 0.09),因为涨的段赚的被跌的段吐回去了;而 OBV 靠「量能确认」在每段下跌开始前离场,把最大回撤从 −19.5% 压到 −8.9%,Sharpe 从 0.09 拉到 1.04。这正是 OBV 的设计意图:它不是在趋势里赚最多,而是在「量能转弱」时先走,把回撤和磨损砍掉。 注意 OBV 交易次数 51 次——它比 CCI 更「多动症」,换手更高,实盘要额外算交易成本(下文陷阱④会讲)。
净值曲线把这件事说得更直白:

绿色曲线(OBV)在每一段下跌 regime 开始前都提前拐平/回落,灰虚线(买入持有)则一路坐过山车。OBV 的贡献是「少亏」而非「多赚」——在震荡交替市里,它把「高买低卖」的反向操作概率压下去。如果是纯单边牛市,OBV 反而会跑输买入持有(它会在中途量能波动里反复上下车),这一点必须心里有数。
五、诚实拆穿:五类真实陷阱#
① 累计偏移:OBV 绝对值无意义,只看「相对变化」。 OBV 从 0 累加,历史上任何一次巨量都会永久改变曲线水平。所以你不能说「OBV=500 万比 OBV=300 万更健康」——这俩可能只是起点不同。所有 OBV 用法都必须基于「斜率、与信号线交叉、与价格背离」,绝不能看绝对值。跨标的比 OBV 更是无意义。
② 无量纲、不可跨标的比。 茅台和某小票的成交量差百倍,OBV 数值也差百倍。要做横向比较,必须归一化(除以各自基准量、或看 OBV 的滚动变化率)。直接用 OBV 数值排「谁量能更强」是典型错误。
③ 噪声日失真:微小涨跌也会 ±整日量。
OBV 把所有涨日量都加、所有跌日量都减,但现实中很多日子是「微涨 0.02% 却放出巨量」或「微跌 0.02% 却缩量」——这些微小方向变化会被 OBV 当成「强信号」累加,制造伪趋势。缓解:对 diff 设阈值(如 |diff| < 0.05% 视为平),或用「加权 OBV」(按涨跌幅比例分配成交量)替代二值加减。
④ 独立使用陷阱:OBV 必须配信号线/过滤器,且换手高。 裸 OBV 斜率噪声极大,单日巨量就能让它猛拐。本文用 EMA20 信号线平滑后才有交易意义;即便如此,51 次交易意味着交易成本敏感——若每次双边成本 0.1%,光手续费就吃掉约 5 个点。实盘必须把印花税/佣金/滑点加回回测,否则 OBV 的「超额」可能是幻觉。
⑤ 参数脆弱:信号线 span 不是金科玉律。 默认 EMA20 是经验值。span 调小(如 10)→ 信号更灵敏、换手更高、噪声更多;span 调大(如 50)→ 更稳但更滞后、易错过早期吸筹。参数要按品种波动率和交易周期重新标定,并做 walk-forward 验证,不能无脑照搬 20。
六、落到量化:你的日常工作流#
- 先复刻公式再谈策略:用上面
compute_obv把 OBV 算出来,对照软件副图逐点核对——很多「策略不灵」的 bug,其实是自己实现的direction符号或prepend处理与平台不一致(注意首根diff的处理)。 - 永远配信号线,别看裸斜率:裸 OBV 噪声大到不可用;至少加一条 EMA 信号线做交叉,或看「OBV 与价格的背离」(价创新高、OBV 不创新高 = 顶背离预警)。
- 把它当「量能确认」而非「收益引擎」:报告里重点看最大回撤、回撤持续时间和换手成本,而不是总收益。OBV 的贡献是「在派发开始时离场」,体现在回撤和磨损项上。
- 必加交易成本重算:OBV 换手高,任何没算手续费的 OBV 回测都不可信。把双边成本(佣金+印花税+滑点)加回,再判断信号是否还为正。
真实落地时,A 股日线直接用
close+ 日成交量序列喂进compute_obv即可;若要做分钟级,注意成交量在集合竞价/尾盘的分布会扭曲 OBV 的日内结构。本文纯 numpy 合成只为把「方向累计 → 信号线交叉」的每一步摊开。记住:OBV 不预测点位,它只回答一个问题——「钱,是在悄悄进来,还是悄悄出去」。
七、CCI 与 OBV:摆荡派 vs 量能派,怎么选#
把本文和上篇 CCI 对照看,会发现两类指标的分工很清晰:
- CCI(摆荡类):基于「价格偏离统计中轴」,擅长震荡市的反转,价值在回撤刹车(最大回撤 −10.1% vs −23.7%)。
- OBV(量能类):基于「成交量的方向累计」,擅长捕捉资金流入流出的拐点,价值在「量在价先」的先导信号(最大回撤 −8.9% vs −19.5%)。
实战里两者不冲突:CCI 管「价格贵不贵、该不该反转」,OBV 管「量能在不在、趋势真不真」。把 CCI 的 ±100 通道反转,配上 OBV 的「量能确认」(只在 OBV 站在信号线上方时才接受 CCI 做多信号),就能用成交量给摆荡信号做一层过滤——这正是多指标融合的基本思路。