Detrended Price Oscillator (DPO)

Parameters: period = 5

Overview

The Detrended Price Oscillator (DPO) isolates short term cycles by removing the underlying trend from price data, making cyclical patterns clearly visible for analysis. Unlike most oscillators that compare current values, DPO uses a unique centered approach by comparing historical price to a displaced moving average. The indicator calculates a simple moving average, then shifts it back by half the period plus one bar. This centered alignment removes the trend component, leaving only the cyclical oscillations around that trend. The mathematical displacement means DPO is not a real time indicator but rather a tool for identifying historical cycle lengths and patterns.

DPO oscillates above and below zero, with peaks and troughs representing cycle highs and lows independent of the overall trend direction. The distance between consecutive peaks or troughs reveals the dominant cycle length in the data. Regular, consistent oscillations indicate stable cyclical behavior, while irregular patterns suggest changing market dynamics or multiple competing cycles. The amplitude of oscillations shows cycle strength, with larger swings indicating more pronounced cyclical movement. Because DPO removes trend, it can show oversold readings in strong uptrends or overbought readings in downtrends, focusing purely on cyclical extremes.

Traders primarily use DPO for cycle analysis rather than generating trading signals. By identifying the dominant cycle length through peak to peak or trough to trough measurements, traders can optimize other indicators to match market rhythm. This cycle information helps time entries and exits by anticipating when the next cycle turn should occur based on historical patterns. Many analysts use DPO in conjunction with trend following indicators, entering positions aligned with the trend during cycle lows. The indicator also assists in distinguishing between trending and cycling market phases, as diminishing DPO amplitude often precedes trend changes or breakouts from consolidation patterns.

Implementation Examples

Get started with DPO in just a few lines:

use vectorta::indicators::dpo::{dpo, DpoInput, DpoParams};
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 = DpoParams { period: Some(5) }; // default period = 5
let input = DpoInput::from_slice(&prices, params);
let result = dpo(&input)?;

// Using with Candles data structure (defaults: period=5, source="close")
let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let input = DpoInput::with_default_candles(&candles);
let result = dpo(&input)?;

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

API Reference

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

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

// From candles with default params (close prices, period=5)
DpoInput::with_default_candles(&Candles) -> DpoInput
Parameters Structure
pub struct DpoParams {
    pub period: Option<usize>, // Default: 5
}
Output Structure
pub struct DpoOutput {
    pub values: Vec<f64>, // DPO values (Price[t - back] - SMA[t])
}
Validation, Warmup & NaNs
  • period > 0; otherwise DpoError::InvalidPeriod.
  • period must not exceed the input length; otherwise DpoError::InvalidPeriod.
  • There must be at least period valid points after the first finite value; otherwise DpoError::NotEnoughValidData.
  • All values NaN leads to DpoError::AllValuesNaN. Empty input yields DpoError::EmptyInputData.
  • Warmup: indices before the first defined output are NaN. Internally, back = period/2 + 1 and warmup is at least max(first_valid + period − 1, back).
  • Streaming: returns None until both SMA and lag buffers are filled (requires period values and at least back + 1 inputs).
Error Handling
#[derive(Debug, Error)]
pub enum DpoError {
    EmptyInputData,                         // dpo: Input data slice is empty.
    AllValuesNaN,                           // dpo: All values are NaN.
    InvalidPeriod { period: usize, data_len: usize },
    NotEnoughValidData { needed: usize, valid: usize },
}

// Matching on errors
match dpo(&input) {
    Ok(output) => { /* use output.values */ },
    Err(DpoError::EmptyInputData) => eprintln!("empty input"),
    Err(DpoError::AllValuesNaN) => eprintln!("all NaN"),
    Err(DpoError::InvalidPeriod { period, data_len }) => {
        eprintln!("invalid period: {} for length {}", period, data_len)
    }
    Err(DpoError::NotEnoughValidData { needed, valid }) => {
        eprintln!("need {} valid values, have {}", needed, valid)
    }
}

Python Bindings

Basic Usage

Calculate DPO using NumPy arrays (default period=5):

import numpy as np
from vectorta import dpo

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

# Calculate DPO with defaults (period=5)
result = dpo(prices, period=5)

# Or specify a different kernel (auto, scalar, avx2, avx512 where available)
result = dpo(prices, period=14, kernel="auto")

print(f"DPO values: {result}")
Streaming Real-time Updates

Process real-time price updates efficiently:

from vectorta import DpoStream

# Initialize streaming DPO calculator
stream = DpoStream(period=5)

# Process real-time price updates
for price in price_feed:
    dpo_value = stream.update(price)
    if dpo_value is not None:
        print(f"Current DPO: {dpo_value}")
Batch Parameter Optimization

Test a range of periods efficiently:

import numpy as np
from vectorta import dpo_batch

prices = np.array([...])

# Define period sweep: (start, end, step)
period_range = (5, 60, 5)

results = dpo_batch(
    prices,
    period_range=period_range,
    kernel="auto"
)

print(f"Values shape: {results['values'].shape}")  # (num_periods, len(prices))
print(f"Periods tested: {results['periods']}")
CUDA Acceleration

CUDA support for DPO is currently under development. The API will follow the same pattern as other CUDA-enabled indicators.

# Coming soon: CUDA-accelerated DPO calculations
#
# from vectorta import dpo_cuda_batch, dpo_cuda_many_series_one_param
# import numpy as np
#
# # Option 1: One Series, Many Parameters (parameter optimization)
# results = dpo_cuda_batch(
#     data=prices,
#     period_range=(5, 60, 1),
#     device_id=0
# )
#
# # Option 2: Many Series, One Parameter Set (portfolio processing)
# portfolio_data = np.array([...])  # Shape: [time_steps, num_assets]
# results = dpo_cuda_many_series_one_param(
#     data_tm=portfolio_data,
#     period=20,
#     device_id=0
# )
#
# # Zero-copy variant with pre-allocated output (F32 for GPU efficiency)
# out = np.empty((time_steps, num_assets), dtype=np.float32)
# dpo_cuda_many_series_one_param_into(
#     data_tm_f32=portfolio_data.astype(np.float32),
#     period=20,
#     out=out,
#     device_id=0
# )

JavaScript/WASM Bindings

Basic Usage

Calculate DPO in JavaScript/TypeScript:

import { dpo_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 DPO with specified period
const result = dpo_js(prices, 5);  // period=5

console.log('DPO values:', result);
Memory-Efficient Operations

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

import { dpo_alloc, dpo_free, dpo_into, memory } from 'vectorta-wasm';

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

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

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

// Calculate DPO directly into allocated memory
// Args: in_ptr, out_ptr, len, period
dpo_into(inPtr, outPtr, length, 5);

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

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

console.log('DPO values:', dpoValues);
Batch Processing

Test multiple period values efficiently:

import { dpo_batch_js } from 'vectorta-wasm';

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

// Define parameter sweep via config object
const config = { period_range: [5, 60, 5] };

// Calculate all combinations
const output = dpo_batch_js(prices, config);

// Output structure: { values, combos, rows, cols }
// values is flat [rows * cols]; reshape if desired
const rows = output.rows;
const cols = output.cols;
const matrix = [];
for (let r = 0; r < rows; r++) {
  const start = r * cols;
  matrix.push(output.values.slice(start, start + cols));
}

// combos holds parameter objects for each row (e.g., { period: 5 })
console.log('First row period =', output.combos[0].period);
console.log('DPO values for first row:', matrix[0]);

Performance Analysis

Comparison:
View:

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

Loading chart...

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

Related Indicators