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::EmptyData for zero-length input; BopError::InputLengthsMismatch if 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 ≤ 0 then 0.0 else (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

Comparison:
View:

Across sizes, Rust CPU runs about 3.43× faster than Tulip C in this benchmark.

Loading chart...

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