On Balance Volume 指标:公式、参数与读法

能量潮(On Balance Volume,OBV)由 Joseph Granville 于 1963 年发表:收盘价上涨时,把该 K 线的全部成交量加入累计值;收盘价下跌时则减去。收盘价不变时,累计值保持原样。

Senzoukria · 指标 · Updated September 2026


On Balance Volume 成交量, 显示在图表下方的独立面板中.

On Balance Volume 测量的是什么

方向由相邻收盘价的严格比较决定:更高则加,更低则减,相等则不变,因此十字星 K 线永远不会被算作看涨。每根 K 线的成交量都是整体计入——该指标不区分收盘价高出一个 tick 还是高出三十个 tick。Senzoukria 在载入的第一根 K 线处将序列起点设为 0,这使绘出的曲线可以复现,但其绝对水平没有意义。累计是连续的,不会在 17:00 CT 开盘时重置;这一锚点只用于按时段计算的指标,例如累计 Delta。

公式:代码中的实现

这不是别处常见的定义,而是所发布代码实际计算的内容,记录在实现旁边:

Période entière ≥ `min` depuis les params (garde-fou UI). */ function intParam( params: Record<string, unknown>, key: string, fallback: number, min = 1, ): number { return Math.max(min, Math.floor(numParam(params, key, fallback))); } function toPoints( bars: readonly FootprintBar[], values: ReadonlyArray<number | null>, ): SeriesPoint[] { return bars.map((b, i) => ({ time: b.bucketTsNs, value: values[i] ?? null })); } /** Points d'histogramme : `f` rend `null` quand la barre n'a pas d'information — la barre n'émet alors PAS de point (contrat `HistogramPoint` : pas de trou, pas de valeur inventée). */ function histPoints( bars: readonly FootprintBar[], f: (bar: FootprintBar, i: number) => number | null, ): HistogramPoint[] { const out: HistogramPoint[] = []; for (let i = 0; i < bars.length; i++) { const v = f(bars[i], i); if (v !== null) out.push({ time: bars[i].bucketTsNs, value: v }); } return out; } function histOut( points: HistogramPoint[], positiveColor: string, negativeColor: string, ): SeriesOutput { return { shape: "histogram", points, positiveColor, negativeColor }; } /** SMA N sur série à trous : null dès qu'UNE valeur de la fenêtre est null — moyenner autour d'un trou mentirait sur la période effective (même règle que `smaNullable` de momentum.ts). */ function smaNullable( values: ReadonlyArray<number | null>, period: number, ): Array<number | null> { const n = Math.max(1, Math.floor(period)); const out: Array<number | null> = new Array<number | null>(values.length).fill(null); for (let i = n - 1; i < values.length; i++) { let sum = 0; let ok = true; for (let j = i - n + 1; j <= i; j++) { const v = values[j]; if (v === null) { ok = false; break; } sum += v; } if (ok) out[i] = sum / n; } return out; } /** EMA N par segment de valeurs définies consécutives. Un trou reste null et réinitialise uniquement la graine suivante : les valeurs antérieures sont conservées. Chaque reprise attend N valeurs pour sa SMA initiale, puis suit la même récurrence que `emaSeries`, sans zéro synthétique. */ function segmentedEma( values: ReadonlyArray<number | null>, period: number, ): Array<number | null> { const n = Math.max(1, Math.floor(period)); const alpha = 2 / (n + 1); const out: Array<number | null> = new Array<number | null>(values.length).fill(null); let count = 0; let sum = 0; let ema = 0; for (let i = 0; i < values.length; i++) { const v = values[i]; if (v === null) { count = 0; sum = 0; continue; } if (count < n) { sum += v; count += 1; if (count < n) continue; ema = sum / n; } else { ema += alpha * (v - ema); } out[i] = ema; } return out; } /** Money Flow Volume de Chaikin pour UNE barre : MFM = ((close − low) − (high − close)) / (high − low) ∈ [−1, +1] MFV = MFM × volume MFM est la position du close dans le range, recentrée : +1 = close au plus haut (toute la barre a été achetée), −1 = close au plus bas. Range NUL (high = low) → `null` : le multiplicateur est un 0/0, la barre ne porte aucune information directionnelle. Les appelants décident quoi en faire — cumul → contribution 0 (cf. en-tête), non cumulé → pas de point. / function moneyFlowVolume(bar: FootprintBar): number | null { const range = bar.high - bar.low; if (!(range > 0)) return null; const mfm = (bar.close - bar.low - (bar.high - bar.close)) / range; return mfm * bar.totalVolume; } /** Ligne A/D (Chaikin) : cumul du Money Flow Volume depuis la première barre chargée. Barre à range nul → contribution 0 (exception documentée en en-tête : un accumulateur ne peut pas porter de trou). CONTINU — pas de reset session (l'A/D est une ligne d'accumulation de long terme). */ function accumDistValues(bars: readonly FootprintBar[]): number[] { const out: number[] = new Array<number>(bars.length); let running = 0; for (let i = 0; i < bars.length; i++) { running += moneyFlowVolume(bars[i]) ?? 0; out[i] = running; } return out; } /** VWMA N sur une fenêtre glissante : Σ(close·volume) / Σ(volume). Σvolume = 0 sur la fenêtre (barres placeholder sans trade) → null : 0/0 n'est pas « 0 ». Warm-up (< N barres) → null. Même formule que `vwma` (averages2.ts) — redéclarée ici plutôt que d'élargir la surface publique d'un autre fichier de famille (sa primitive n'est pas exportée). */ function vwmaSeries( bars: readonly FootprintBar[], period: number, ): Array<number | null> { const n = Math.max(1, Math.floor(period)); const out: Array<number | null> = new Array<number | null>(bars.length).fill(null); let sumPV = 0; let sumV = 0; for (let i = 0; i < bars.length; i++) { sumPV += bars[i].close * bars[i].totalVolume; sumV += bars[i].totalVolume; if (i >= n) { sumPV -= bars[i - n].close * bars[i - n].totalVolume; sumV -= bars[i - n].totalVolume; } if (i >= n - 1) out[i] = sumV > 0 ? sumPV / sumV : null; } return out; } /** Indices de volume de Fosback/Dysart (NVI / PVI) — même moteur, deux déclencheurs : index[0] = base index[i] = index[i−1] × (1 + (close[i] − close[i−1]) / close[i−1]) si `trigger(volume[i], volume[i−1])`, sinon index[i−1] NVI : trigger = volume EN BAISSE (Fosback — l'« argent malin » se positionne les jours calmes). PVI : trigger = volume EN HAUSSE (la foule). Volume ÉGAL → aucun des deux ne bouge (inégalités STRICTES, convention d'origine : une journée sans changement de participation n'est ni l'un ni l'autre). close[i−1] = 0 → contribution 0, l'indice est GELÉ (exception documentée en en-tête : le ratio n'existe pas, et un null perdrait l'ancrage de toute la suite de l'indice — un indice base 1000 sans ancrage ne vaut rien). / function volumeIndexValues( bars: readonly FootprintBar[], base: number, trigger: (vol: number, prevVol: number) => boolean, ): number[] { const out: number[] = new Array<number>(bars.length); let idx = base; for (let i = 0; i < bars.length; i++) { if (i > 0) { const prev = bars[i - 1]; const prevClose = prev.close; if (trigger(bars[i].totalVolume, prev.totalVolume) && prevClose !== 0) { idx *= 1 + (bars[i].close - prevClose) / prevClose; } } out[i] = idx; } return out; } // ── Defs ──────────────────────────────────────────────────────────────────── /** OBV — On Balance Volume (Granville, 1963) : OBV[0] = 0 (graine, cf. convention) OBV[i] = OBV[i−1] + volume[i] si close[i] > close[i−1] = OBV[i−1] − volume[i] si close[i] < close[i−1] = OBV[i−1] si close[i] = close[i−1] POURQUOI : la thèse de Granville est que le volume précède le prix. L'OBV signe le volume ENTIER de la barre par la direction du close — sa PENTE et ses divergences avec le prix sont l'information, jamais son niveau absolu. Convention GRAINE : OBV[0] = 0. L'indicateur n'a pas d'origine absolue (Granville lui-même part d'un nombre arbitraire) ; ancrer à 0 sur la première barre CHARGÉE rend la série reproductible et lisible. Corollaire assumé : charger plus d'historique décale toute la courbe — c'est vrai de tout OBV, ATAS et TradingView compris. Convention ÉGALITÉ : close inchangé → OBV inchangé (règle de Granville, inégalités STRICTES — pas de « ≥ » qui compterait les dojis comme haussiers). CONTINU : pas de reset de session — l'ancrage 17:00 CT ne concerne que les cumuls orderflow type CVD, l'OBV est une accumulation de long terme.

如何解读

  • Granville 的观点是量在价先;不论你是否认同,这条线真正可用的信息是它的斜率以及它与价格的背离,而不是它的数值水平。
  • 价格横盘而 OBV 低点逐步抬高,说明在这段时间里,收盘下跌的 K 线成交量小于收盘上涨的 K 线。
  • 价格创新高而 OBV 没有同步创新高,说明支撑这个高点的上涨收盘所带的成交量少于之前——这是关于这些 K 线的事实,而不是信号。
  • 由于载入更多历史数据会让整条曲线平移,不要依据 OBV 的某个阈值做任何判断,应依据它的形态。
  • 在日内期货上,把 OBV 与累计 Delta 对照:OBV 按收盘方向给成交量赋号,Delta 按主动方赋号,两者出现分歧的地方正是发生吸收的地方。

参数与默认值

平滑默认值为 1,保持原始累计线不变;最高 200 的数值会做一次事后平滑,让长周期斜率更易观察,同时隐藏产生它的单根 K 线。默认颜色为蓝色(#2962ff)。

On Balance Volume — 应用中可调的参数及其默认值。
参数类型默认值取值范围
Smoothingnumber11 – 200

它无法显示的内容

OBV 是全有或全无的:上涨一个 tick 与大幅上涨的贡献完全相同,因此在经常以几乎不变的价格收盘的品种上,这条线会跳动剧烈。它只使用收盘价,K 线内部发生的一切都看不到,包括吸收和主动方。起点为 0 使其绝对数值没有意义,也无法在不同图表或不同历史长度之间比较——所有 OBV 实现都有这一特性,包括 ATAS 和 TradingView 中的实现。收盘价相等时曲线静止,因此取整严重或精度较低的价格数据会出于机械原因把这条线压平。

浏览全部指标

其他语言版本

Frequently asked questions

能量潮 OBV 如何计算?
从一个起始值出发,当某根 K 线的收盘价高于前一根收盘价时加上它的全部成交量,低于时则减去。两根收盘价相等时,累计值不变。在 Senzoukria 中,起始值为图表中载入的第一根 K 线处的 0。
OBV 会在时段开盘时重置吗?
不会。OBV 跨时段连续计算,因为 Granville 把它设计为长周期累积指标。软件中的 17:00 CT 时段锚点适用于按时段累计的指标,例如累计 Delta 或时段成交量,不适用于 OBV。
OBV 与累计 Delta 有什么区别?
OBV 按收盘方向给 K 线的成交量赋号,因此只依赖 OHLC 数据。累计 Delta 按每笔成交的主动方给成交量赋号,需要数据源提供 bid/ask 分类。当主动成交量没有推动收盘价时两者就会背离,这正是吸收的特征。

Keep reading