Absolute Price Oscillator (APO)

Parameters: short_period = 10 | long_period = 20

Overview

APO quantifies momentum by subtracting a long exponential moving average from a short EMA, displaying the result in absolute price units rather than percentages. When the fast EMA rises above the slow EMA, APO turns positive and indicates upward momentum proportional to the price difference. The calculation commonly uses a 12 period fast EMA and 26 period slow EMA, though traders adjust these based on their timeframe. Unlike percentage oscillators that normalize values, APO preserves the actual dollar or point difference between averages, making momentum directly comparable across different time periods for the same instrument. Traders use APO to identify trend strength when values expand from zero, spot divergences when price makes new highs while APO declines, and time entries when the oscillator crosses above zero after a pullback. The absolute price format particularly benefits traders who need to understand momentum in terms of actual price movement rather than relative percentages.

Implementation Examples

Get started with APO in just a few lines:

use vector_ta::indicators::apo::{apo, ApoInput, ApoParams};
use vector_ta::utilities::data_loader::{Candles, read_candles_from_csv};

// Using with price data slice (need at least long_period values)
let prices: Vec<f64> = vec![/* ... */];
let params = ApoParams {
    short_period: Some(10),
    long_period: Some(20),
};
let input = ApoInput::from_slice(&prices, params);
let result = apo(&input)?;

// Using with Candles data structure
// Quick and simple with default parameters (short=10, long=20; source="close")
let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let input = ApoInput::with_default_candles(&candles);
let result = apo(&input)?;

// Access the APO values
for value in result.values {
    println!("APO: {}", value);
}

API Reference

Input Methods
// From price slice
ApoInput::from_slice(&[f64], ApoParams) -> ApoInput

// From candles with custom source
ApoInput::from_candles(&Candles, &str, ApoParams) -> ApoInput

// From candles with default params (close prices, 10/20 periods)
ApoInput::with_default_candles(&Candles) -> ApoInput
Parameters Structure
pub struct ApoParams {
    pub short_period: Option<usize>, // Default: 10
    pub long_period: Option<usize>,  // Default: 20
}
Output Structure
pub struct ApoOutput {
    pub values: Vec<f64>, // APO values (short_ema - long_ema)
}
Validation, Warmup & NaNs
  • short_period > 0, long_period > 0, and short_period < long_period.
  • There must be at least long_period valid points after the first finite value; otherwise ApoError::NotEnoughValidData.
  • Indices before the first finite input are NaN. The first finite output is 0.0 (both EMAs seeded to that price).
  • Streaming: leading NaNs delay the first value; subsequent NaNs propagate.
Error Handling
use vector_ta::indicators::apo::ApoError;

match apo(&input) {
    Ok(output) => process_results(output.values),
    Err(ApoError::EmptyInputData) =>
        println!("Input data is empty"),
    Err(ApoError::ShortPeriodNotLessThanLong { short, long }) =>
        println!("Short period {} must be < long period {}", short, long),
    Err(ApoError::NotEnoughValidData { needed, valid }) =>
        println!("Need {} data points, only {} valid", needed, valid),
    Err(e) => println!("APO error: {}", e)
}

Python Bindings

Basic Usage

Calculate APO using NumPy arrays (defaults: short=10, long=20):

import numpy as np
from vector_ta import apo

# Prepare price data as NumPy array
prices = np.array([100.0, 102.0, 101.5, 103.0, 105.0, 104.5])

# Calculate APO with defaults (10/20)
result = apo(prices)

# Or specify custom parameters
result = apo(prices, short_period=10, long_period=20)

# Specify kernel for performance optimization
result = apo(prices, short_period=10, long_period=20, kernel="avx2")

# Result is a NumPy array matching input length
print(f"APO values: {result}")
Streaming Real-time Updates

Process real-time price updates efficiently:

from vector_ta import ApoStream

# Initialize streaming APO calculator
stream = ApoStream(short_period=10, long_period=20)

# Process real-time price updates
for price in price_feed:
    apo_value = stream.update(price)

    if apo_value is not None:
        # APO value is ready (None during warmup period)
        print(f"Current APO: {apo_value}")

        # Make trading decisions based on APO
        if apo_value > 0:
            print("Momentum is positive")
        else:
            print("Momentum is negative")
Batch Parameter Optimization

Test multiple parameter combinations for optimization:

import numpy as np
from vector_ta import apo_batch

# Your price data
prices = np.array([...])  # Your historical prices

# Define parameter ranges to test
# (start, end, step) for each parameter
short_range = (5, 20, 5)    # Test: 5, 10, 15, 20
long_range = (15, 50, 5)    # Test: 15, 20, 25, ..., 50

# Run batch calculation
results = apo_batch(
    prices,
    short_period_range=short_range,
    long_period_range=long_range,
    kernel="auto"  # Auto-select best kernel
)

# Access results
print(f"Values shape: {results['values'].shape}")  # (num_combinations, len(prices))
print(f"Short periods tested: {results['short_periods']}")
print(f"Long periods tested: {results['long_periods']}")

# Find best performing parameters
best_idx = np.argmax(some_performance_metric(results['values']))
best_short = results['short_periods'][best_idx]
best_long = results['long_periods'][best_idx]
CUDA Acceleration

CUDA helpers are available when the Python package is built with CUDA support. Inputs must be float32; outputs are device arrays (DLPack / __cuda_array_interface__ compatible).

import numpy as np
from vector_ta import apo_cuda_batch_dev, apo_cuda_many_series_one_param_dev

# One series (float32)
data_f32 = np.asarray(load_data(), dtype=np.float32)

dev = apo_cuda_batch_dev(
    data_f32=data_f32,
    short_range=(2, 20, 2),
    long_range=(2, 20, 2),
    device_id=0,
)

# Many series (time-major)
data_tm_f32 = np.asarray(load_data_time_major_matrix(), dtype=np.float32)

dev_tm = apo_cuda_many_series_one_param_dev(
    data_tm_f32=data_tm_f32,
    short_period=14,
    long_period=14,
    device_id=0,
)

JavaScript/WASM Bindings

Basic Usage

Calculate APO in JavaScript/TypeScript:

import { apo_js } from 'vectorta-wasm';

// Price data as Float64Array or regular array
const prices = new Float64Array([100.0, 102.0, 101.5, 103.0, 105.0, 104.5]);

// Calculate APO with specified parameters
const result = apo_js(prices, 10, 20);  // short=10, long=20

// Result is a Float64Array
console.log('APO values:', result);

// TypeScript type definitions
interface ApoResult {
  values: Float64Array;
}

// Use with async/await for better error handling
async function calculateAPO(prices: Float64Array): Promise<Float64Array> {
  try {
    return apo_js(prices, 10, 20);
  } catch (error) {
    console.error('APO calculation failed:', error);
    throw error;
  }
}
Memory-Efficient Operations

Use zero-copy operations for better performance with large datasets:

import { apo_alloc, apo_free, apo_into, memory } from 'vectorta-wasm';

// Prepare your price data
const prices = new Float64Array([/* your data */]);
const length = prices.length;

// Allocate WASM memory for input and output
const inPtr = apo_alloc(length);
const outPtr = apo_alloc(length);

// Copy input data into WASM memory
new Float64Array(memory.buffer, inPtr, length).set(prices);

// Calculate APO directly into allocated memory
// Args: in_ptr, out_ptr, len, short_period, long_period
apo_into(inPtr, outPtr, length, 10, 20);

// Read results from WASM memory (slice() to copy out)
const apoValues = new Float64Array(memory.buffer, outPtr, length).slice();

// Free allocated memory when done
apo_free(inPtr, length);
apo_free(outPtr, length);

console.log('APO values:', apoValues);
Batch Processing

Test multiple parameter combinations efficiently:

import { apo_batch_js, apo_batch_metadata_js } from 'vectorta-wasm';

// Your price data
const prices = new Float64Array([/* historical prices */]);

// Define parameter sweep ranges
const shortStart = 5, shortEnd = 20, shortStep = 5;  // 5, 10, 15, 20
const longStart = 15, longEnd = 50, longStep = 5;   // 15, 20, 25...50

// Get metadata about parameter combinations
const metadata = apo_batch_metadata_js(
  shortStart, shortEnd, shortStep,
  longStart, longEnd, longStep
);

// metadata contains [short1, long1, short2, long2, ...]
const numCombos = metadata.length / 2;

// Calculate all combinations
const results = apo_batch_js(
  prices,
  shortStart, shortEnd, shortStep,
  longStart, longEnd, longStep
);

// Results is a flat array: [combo1_values..., combo2_values..., ...]
// Reshape based on your needs
const resultMatrix = [];
for (let i = 0; i < numCombos; i++) {
  const start = i * prices.length;
  const end = start + prices.length;
  resultMatrix.push(results.slice(start, end));
}

// Access specific parameter combination results
const shortPeriod = metadata[0];  // First combo's short period
const longPeriod = metadata[1];   // First combo's long period
const apoValues = resultMatrix[0]; // First combo's APO values

CUDA Bindings (Rust)

use vector_ta::cuda::CudaApo;
use vector_ta::indicators::apo::ApoBatchRange;

let cuda = CudaApo::new(0)?;

let data_f32: [f32] = /* ... */;
let sweep = ApoBatchRange::default();

let out = cuda.apo_batch_dev(&data_f32, &sweep)?;
let _ = out;

Performance Analysis

Comparison:
View:

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

Loading chart...

AMD Ryzen 9 9950X (CPU) | NVIDIA RTX 4090 (GPU) | Benchmarks: 2026-02-28

Related Indicators