UniversalOscillator
Ehlers' Universal Oscillator — whitens the price, SuperSmooths it, then AGC-normalises to a clean
[−1, +1]cycle reading on any instrument.
Quick reference
| Field | Value |
|---|---|
| Family | Ehlers / Cycle (DSP) |
| Input type | f64 |
| Output type | f64 |
| Output range | [−1, +1] |
| Default parameters | (period = 20) (Python) |
| Warmup period | 3 |
| Interpretation | Rails = cycle extremes; zero-cross = cycle direction change. |
Formula
WhiteNoise = (price_t − price_{t−2}) / 2
Filt = SuperSmoother(WhiteNoise, period)
Peak = max(|Filt|, 0.991 · Peak_{t−1})
Universal = Filt / Peak (0 if Peak == 0)Whitening (the two-bar difference) flattens the input's power spectrum so the SuperSmoother treats all cycles even-handedly rather than being dominated by the trend. The automatic gain control divides by a slowly-decaying running peak, so the amplitude is normalised to [−1, +1] regardless of volatility — hence "universal". Source: crates/wickra-core/src/indicators/universal_oscillator.rs.
Parameters
| Name | Type | Default | Valid range | Source | Description |
|---|---|---|---|---|---|
period | usize | 20 (Python) | >= 1 | universal_oscillator.rs:55 | SuperSmoother length applied to the whitened series. 0 errors with Error::PeriodZero. |
The period getter returns the smoother length; value returns the current output if ready.
Inputs / Outputs
From crates/wickra-core/src/indicators/universal_oscillator.rs:
use wickra::{Indicator, UniversalOscillator};
// UniversalOscillator: Input = f64, Output = f64
const _: fn(&mut UniversalOscillator, f64) -> Option<f64> =
<UniversalOscillator as Indicator>::update;An f64 in, an Option<f64> out. The Python binding takes a scalar for update and a 1-D numpy array for batch (NaN warmup); Node takes update(value) and batch(values[]).
Warmup
warmup_period() == 3. A two-bar difference needs two prior prices; the first value lands on the third bar (first_emission_at_warmup_period pins this).
Edge cases
- Flat input → 0. A constant whitens to zero, so the output is
0(constant_input_is_zeropins this). - Bounded. The AGC keeps the reading in
[−1, +1](output_in_rangepins this). - Cyclic input → both signs. A sine input swings across zero (
cyclic_input_swings_both_signspins this). - Non-finite input. A NaN/∞ input is ignored and the last value returned (
ignores_non_finitepins this). - Reset.
u.reset()clears the smoother, the price history, the AGC peak and the last value (reset_clears_state).
Examples
Rust
use wickra::{BatchExt, Indicator, UniversalOscillator};
use std::f64::consts::TAU;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut u = UniversalOscillator::new(20)?;
let xs: Vec<f64> = (0..200).map(|i| 100.0 + (TAU * f64::from(i) / 20.0).sin() * 5.0).collect();
println!("last = {:?}", u.batch(&xs).last().unwrap());
Ok(())
}Python
import numpy as np
import wickra as ta
u = ta.UniversalOscillator(20)
x = 100 + np.sin(2 * np.pi * np.arange(200) / 20) * 5
print(u.batch(x)[-5:]) # in [-1, 1]Node
const ta = require('wickra');
const u = new ta.UniversalOscillator(20);
console.log('warmupPeriod:', u.warmupPeriod()); // 3Streaming
use wickra::{Indicator, UniversalOscillator};
let mut u = UniversalOscillator::new(20).unwrap();
let mut last = None;
for i in 0..80 {
last = u.update(100.0 + (f64::from(i) * 0.3).sin() * 5.0);
}
println!("{last:?}");Streaming update and batch are equivalent tick-for-tick (batch_equals_streaming pins this).
Interpretation
- Cycle timing. Treat it like a stochastic/RSI bounded oscillator — extremes near
±1are over-extended, zero-crosses mark cycle direction shifts. - Cross-instrument thresholds. Because of the AGC, the same overbought/oversold levels work on any market.
- Confirmation. Pair with a trend filter; the Universal Oscillator times pullbacks within the trend.
Common pitfalls
- Whitening removes the trend. This is a cycle tool; it says nothing about direction on its own.
- AGC lag. After a volatility collapse the peak decays slowly (
0.991/bar), so amplitude normalisation lags a regime change briefly. - Period role.
periodsmooths the whitened series; it is not a cycle length.
References
Ehlers, J. F. (2013), Cycle Analytics for Traders, Wiley — the Universal Oscillator (whitening + SuperSmoother + AGC).
See also
- Indicator-EvenBetterSinewave — normalised sinewave oscillator.
- Indicator-Reflex — zero-lag cycle oscillator.
- Indicator-SuperSmoother — the smoothing core.
- Indicators-Overview — the full taxonomy.