Percentage Price Oscillator (PPO)
fast_period = 12 | slow_period = 26 | ma_type = sma Overview
The Percentage Price Oscillator (PPO) normalizes momentum by expressing the difference between fast and slow moving averages as a percentage of the slower baseline. Rather than reporting absolute point differences like MACD, PPO divides the gap by the slow moving average and multiplies by 100, producing a scale-independent momentum reading. This percentage normalization enables direct comparison of momentum strength across assets trading at vastly different price levels, from penny stocks to high-priced indices. Traders use PPO to identify momentum shifts through centerline crosses, divergences with price action, and relative strength comparisons across multiple instruments. The indicator supports various moving average types, with VectorTA defaulting to SMA 12/26 while many implementations favor EMA for greater responsiveness. Positive PPO values signal that short term momentum exceeds longer term trends, while negative readings indicate weakening momentum relative to the baseline.
Defaults (VectorTA): fast=12, slow=26, ma_type="sma". Typical elsewhere: EMA 12/26.
Implementation Examples
Get started with PPO in just a few lines:
use vectorta::indicators::ppo::{ppo, PpoInput, PpoParams};
use vectorta::utilities::data_loader::{Candles, read_candles_from_csv};
// Using with price data slice
let prices = vec![100.0, 102.0, 101.5, 103.0, 105.0, 104.5];
let params = PpoParams {
fast_period: Some(12),
slow_period: Some(26),
ma_type: Some("sma".to_string()),
};
let input = PpoInput::from_slice(&prices, params);
let result = ppo(&input)?;
// Using with Candles data structure (defaults: 12/26 SMA on "close")
let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let input = PpoInput::with_default_candles(&candles);
let result = ppo(&input)?;
// Access the PPO values
for value in result.values {
println!("PPO: {}%", value);
} API Reference
Input Methods ▼
// From price slice
PpoInput::from_slice(&[f64], PpoParams) -> PpoInput
// From candles with custom source
PpoInput::from_candles(&Candles, &str, PpoParams) -> PpoInput
// From candles with default params (SMA, 12/26, close prices)
PpoInput::with_default_candles(&Candles) -> PpoInput Parameters Structure ▼
pub struct PpoParams {
pub fast_period: Option<usize>, // Default: 12
pub slow_period: Option<usize>, // Default: 26
pub ma_type: Option<String>, // Default: "sma"
} Output Structure ▼
pub struct PpoOutput {
pub values: Vec<f64>, // PPO values in percent: 100 * (fast - slow) / slow
} Validation, Warmup & NaNs ▼
- Warmup prefix: indices
..(first + slow - 1)areNaN, wherefirstis the first non‑NaN input index. PpoError::AllValuesNaNif all inputs areNaN.PpoError::InvalidPeriodiffast == 0,slow == 0, or either period exceeds input length.PpoError::NotEnoughValidDataiflen - first < slow; need at leastslowvalid values after the first.- Computation: if
slow_ma == 0or either MA isNaN, PPO at that index isNaN. - Unknown
ma_type: falls back to MA engine; on failure, fills tail withNaN(no error).
Error Handling ▼
use vectorta::indicators::ppo::PpoError;
match ppo(&input) {
Ok(output) => process_results(output.values),
Err(PpoError::AllValuesNaN) =>
println!("All input values are NaN"),
Err(PpoError::InvalidPeriod { fast, slow, data_len }) =>
println!("Invalid period: fast={}, slow={}, len={}", fast, slow, data_len),
Err(PpoError::NotEnoughValidData { needed, valid }) =>
println!("Need {} data points after first valid, only {}", needed, valid),
Err(PpoError::MaError(e)) =>
println!("MA engine error: {}", e),
} Python Bindings
Basic Usage ▼
Calculate PPO using NumPy arrays (defaults: fast=12, slow=26, ma_type="sma"):
import numpy as np
from vectorta import ppo
# Prepare price data as NumPy array
prices = np.array([100.0, 102.0, 101.5, 103.0, 105.0, 104.5])
# Calculate PPO with defaults (12/26 SMA)
result = ppo(prices)
# Or specify custom parameters and kernel
result = ppo(prices, fast_period=12, slow_period=26, ma_type="ema", kernel="avx2")
print(f"PPO values (%): {result}") Streaming Real-time Updates ▼
Process real-time price updates efficiently:
from vectorta import PpoStream
# Initialize streaming PPO calculator
stream = PpoStream(fast_period=12, slow_period=26, ma_type="sma")
# Process real-time price updates
for price in price_feed:
ppo_value = stream.update(price)
if ppo_value is not None:
print(f"Current PPO: {ppo_value}")
Batch Parameter Optimization ▼
Test multiple parameter combinations:
import numpy as np
from vectorta import ppo_batch
prices = np.array([...]) # Your historical prices
# Define parameter ranges: (start, end, step)
fast_range = (6, 18, 6) # 6, 12, 18
slow_range = (20, 40, 10) # 20, 30, 40
results = ppo_batch(
prices,
fast_period_range=fast_range,
slow_period_range=slow_range,
ma_type="ema",
kernel="auto"
)
print(results["values"].shape) # (num_combinations, len(prices))
print(results["fast_periods"])
print(results["slow_periods"])
print(results["ma_types"]) CUDA Acceleration ▼
CUDA support for PPO is currently under development. The API will follow the same pattern as other CUDA-enabled indicators.
# Coming soon: CUDA-accelerated PPO calculations
#
# from vectorta import ppo_cuda_batch, ppo_cuda_many_series_one_param
# import numpy as np
#
# # Option 1: One Series, Many Parameters (parameter optimization)
# results = ppo_cuda_batch(
# data=prices,
# fast_period_range=(6, 24, 1),
# slow_period_range=(20, 50, 1),
# ma_type="ema",
# device_id=0
# )
#
# # Option 2: Many Series, One Parameter Set (portfolio processing)
# portfolio_data = np.array([...]) # Shape: [time_steps, num_assets]
# results = ppo_cuda_many_series_one_param(
# data_tm=portfolio_data,
# fast_period=12,
# slow_period=26,
# ma_type="ema",
# device_id=0
# ) JavaScript/WASM Bindings
Basic Usage ▼
Calculate PPO in JavaScript/TypeScript:
import { ppo_js } from 'vectorta-wasm';
const prices = new Float64Array([100.0, 102.0, 101.5, 103.0, 105.0, 104.5]);
// fast=12, slow=26, ma_type="ema"
const result = ppo_js(prices, 12, 26, "ema");
console.log('PPO values (%):', result); Memory-Efficient Operations ▼
Use zero-copy operations for better performance with large datasets:
import { ppo_alloc, ppo_free, ppo_into, memory } from 'vectorta-wasm';
const prices = new Float64Array([/* your data */]);
const length = prices.length;
// Allocate WASM memory for input and output
const inPtr = ppo_alloc(length);
const outPtr = ppo_alloc(length);
// Copy input data into WASM memory
new Float64Array(memory.buffer, inPtr, length).set(prices);
// Calculate PPO directly into allocated memory
// Args: in_ptr, out_ptr, len, fast, slow, ma_type
ppo_into(inPtr, outPtr, length, 12, 26, "ema");
// Read results and free memory
const ppoValues = new Float64Array(memory.buffer, outPtr, length).slice();
ppo_free(inPtr, length);
ppo_free(outPtr, length);
console.log('PPO values (%):', ppoValues); Batch Processing ▼
Test multiple parameter combinations efficiently:
import { ppo_batch as ppo_batch_js } from 'vectorta-wasm';
const prices = new Float64Array([/* historical prices */]);
// Unified batch API returns values + combos metadata
const config = {
fast_period_range: [6, 18, 6],
slow_period_range: [20, 40, 10],
ma_type: 'ema',
};
const { values, combos, rows, cols } = ppo_batch_js(prices, config);
// values is a flat array of length rows * cols
// combos is an array of parameter objects: { fast_period, slow_period, ma_type }
function rowValues(row: number): Float64Array {
const start = row * cols;
return Float64Array.from(values.slice(start, start + cols));
}
// Example: read first combo's PPO series
const firstComboSeries = rowValues(0);
console.log(combos[0], firstComboSeries); Performance Analysis
Across sizes, Rust CPU runs about 2.87× faster than Tulip C in this benchmark.
AMD Ryzen 9 9950X (CPU) | NVIDIA RTX 4090 (GPU) | Benchmarks: 2026-01-05