Balance of Power (BOP)
Overview
Igor Livshin introduced the Balance of Power indicator in August 2001 through his article "Balance of Market Power" in Stocks and Commodities magazine, creating a unique oscillator that measures who controls each price bar by examining the relationship between opens and closes relative to the trading range. The indicator calculates the distance from open to close divided by the distance from high to low, producing values between -1 and +1 that reveal whether buyers or sellers dominated each period. While Livshin's original methodology involved complex calculations, the formula simplifies elegantly to (Close - Open) / (High - Low), making it computationally efficient while preserving the essential market dynamics. When BOP reads positive, buyers pushed prices higher from open to close, with values near +1 indicating complete buyer control where prices opened at the low and closed at the high. Conversely, negative readings show seller dominance, with -1 representing a bar that opened at the high and closed at the low. Livshin recommended smoothing raw BOP values with a 14-period moving average to filter noise and generate clearer signals, particularly at zero-line crossovers where market control shifts between buyers and sellers. The indicator excels at identifying divergences between price action and internal strength, warning when new price extremes lack the corresponding power shifts that would confirm the move's sustainability.
Implementation Examples
Compute BOP from OHLC slices or candles:
use vectorta::indicators::bop::{bop, BopInput, BopParams};
use vectorta::utilities::data_loader::{Candles, read_candles_from_csv};
// Using raw OHLC slices
let open = vec![10.0, 5.0, 6.0, 10.0, 11.0];
let high = vec![15.0, 6.0, 9.0, 20.0, 13.0];
let low = vec![10.0, 5.0, 4.0, 10.0, 11.0];
let close = vec![14.0, 6.0, 7.0, 12.0, 12.0];
let input = BopInput::from_slices(&open, &high, &low, &close, BopParams::default());
let result = bop(&input)?;
// Using with Candles (open/high/low/close sources)
let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let input = BopInput::with_default_candles(&candles);
let result = bop(&input)?;
// Access the BOP values
for value in result.values {
println!("BOP: {}", value);
} API Reference
Input Methods ▼
// From OHLC slices
BopInput::from_slices(&[f64], &[f64], &[f64], &[f64], BopParams) -> BopInput
// From candles (uses open/high/low/close sources)
BopInput::from_candles(&Candles, BopParams) -> BopInput
// Candles with default params
BopInput::with_default_candles(&Candles) -> BopInput Parameters Structure ▼
pub struct BopParams {
// Currently empty; reserved for future options
} Output Structure ▼
pub struct BopOutput {
pub values: Vec<f64>, // BOP values per input bar
} Validation, Warmup & NaNs ▼
- Errors:
BopError::EmptyDatafor zero-length input;BopError::InputLengthsMismatchif OHLC lengths differ. - Warmup: indices before the first all‑finite OHLC sample are
NaN(prefix fill). - Computation starts at first valid index; per‑bar rule: if
high − low ≤ 0then0.0else(close − open)/(high − low). - Streaming:
BopStream::update(o,h,l,c)returns the value immediately (no warmup state).
Error Handling ▼
use vectorta::indicators::bop::{bop, BopInput, BopParams, BopError};
let input = BopInput::from_slices(&open, &high, &low, &close, BopParams::default());
match bop(&input) {
Ok(out) => println!("{} values", out.values.len()),
Err(BopError::EmptyData) => eprintln!("bop: Input data is empty."),
Err(BopError::InputLengthsMismatch { open_len, high_len, low_len, close_len }) => {
eprintln!("bop: Input lengths mismatch - open: {open_len}, high: {high_len}, low: {low_len}, close: {close_len}");
}
} Python Bindings
Basic Usage ▼
Compute BOP from NumPy arrays; optionally select a kernel:
import numpy as np
from vectorta import bop
open = np.array([10.0, 5.0, 6.0, 10.0, 11.0], dtype=float)
high = np.array([15.0, 6.0, 9.0, 20.0, 13.0], dtype=float)
low = np.array([10.0, 5.0, 4.0, 10.0, 11.0], dtype=float)
close = np.array([14.0, 6.0, 7.0, 12.0, 12.0], dtype=float)
# Kernel is optional: "auto", "scalar", "avx2", "avx512" (if available)
values = bop(open, high, low, close, kernel="auto")
print(values) Streaming Real-time Updates ▼
State-free update per bar using OHLC inputs:
from vectorta import BopStream
stream = BopStream()
for (o, h, l, c) in ohlc_feed:
val = stream.update(o, h, l, c)
process(val) Batch Processing ▼
BOP has no tunable parameters; batch returns a single row:
import numpy as np
from vectorta import bop_batch
open, high, low, close = np.array([...]), np.array([...]), np.array([...]), np.array([...])
out = bop_batch(open, high, low, close, kernel="auto")
print(out['values'].shape) # (1, len)
print(out['rows'], out['cols']) CUDA Acceleration ▼
CUDA support for BOP is currently under development. The API will follow the same pattern as other CUDA-enabled indicators.
# Coming soon: CUDA-accelerated BOP calculations
# from vectorta import bop_cuda_batch
# ... JavaScript/WASM Bindings
Basic Usage ▼
Calculate BOP from OHLC arrays:
import { bop_js } from 'vectorta-wasm';
const open = new Float64Array([10, 5, 6, 10, 11]);
const high = new Float64Array([15, 6, 9, 20, 13]);
const low = new Float64Array([10, 5, 4, 10, 11]);
const close = new Float64Array([14, 6, 7, 12, 12]);
const values = bop_js(open, high, low, close);
console.log('BOP:', values); Memory-Efficient Operations ▼
Use zero-copy pointers for large datasets:
import { bop_alloc, bop_free, bop_into, memory } from 'vectorta-wasm';
const open = new Float64Array([/* ... */]);
const high = new Float64Array([/* ... */]);
const low = new Float64Array([/* ... */]);
const close = new Float64Array([/* ... */]);
const len = open.length;
// Allocate WASM buffers
const outPtr = bop_alloc(len);
const openPtr = bop_alloc(len);
const highPtr = bop_alloc(len);
const lowPtr = bop_alloc(len);
const closePtr= bop_alloc(len);
// Copy input arrays into WASM memory
new Float64Array(memory.buffer, openPtr, len).set(open);
new Float64Array(memory.buffer, highPtr, len).set(high);
new Float64Array(memory.buffer, lowPtr, len).set(low);
new Float64Array(memory.buffer, closePtr,len).set(close);
// Compute directly into pre-allocated output
bop_into(openPtr, highPtr, lowPtr, closePtr, outPtr, len);
// Read results (slice() to copy out of WASM memory)
const values = new Float64Array(memory.buffer, outPtr, len).slice();
// Free WASM buffers
[openPtr, highPtr, lowPtr, closePtr, outPtr].forEach(ptr => bop_free(ptr, len)); Batch Processing ▼
BOP has no parameters; batch returns a single row:
import { bop_batch_js, bop_batch_metadata_js } from 'vectorta-wasm';
const values = bop_batch_js(open, high, low, close);
const metadata = bop_batch_metadata_js(); // Empty for BOP
console.log(values.length, metadata.length); Performance Analysis
Across sizes, Rust CPU runs about 3.43× faster than Tulip C in this benchmark.
AMD Ryzen 9 9950X (CPU) | NVIDIA RTX 4090 (GPU) | Benchmarks: 2026-01-05
CUDA note
In our benchmark workload, the Rust CPU implementation is faster than CUDA for this indicator. Prefer the Rust/CPU path unless your workload differs.
Related Indicators
Accumulation/Distribution
Technical analysis indicator
Accumulation/Distribution Oscillator
Technical analysis indicator
Chaikin Flow Oscillator
Technical analysis indicator
Elder Force Index
Technical analysis indicator
Ease of Movement
Technical analysis indicator
Klinger Volume Oscillator
Technical analysis indicator