The original taxonomy was four classical families plus a statistics group, with the F1-F12 expansion slotted in as sub-categories. This regroups the whole 71-indicator catalogue into eight top-level families, each with at least five members: Moving Averages (12), Momentum Oscillators (13), Trend & Directional (9), Price Oscillators (5), Volatility & Bands (12), Trailing Stops (5), Volume (9), Price Statistics (7). - Wiki: docs/wiki/indicators/ reorganised into eight family folders; all 71 indicator pages moved with `git mv`. Every internal cross-link is normalised to `../<family>/Indicator-X.md`, each page's `Family` field is set to its new family, and two pre-existing `../Indicator-Chaining.md` links (should have been `../../`) are corrected. A link check confirms every relative wiki link resolves. - Indicators-Overview.md fully rewritten around the eight families; Home.md indicator reference and the README family table follow suit. - Warmup-Periods.md gains the eight F13 indicators; CHANGELOG records the 46-indicator expansion (25 -> 71) and the eight-family taxonomy. - Tests: Node indicators.test.js and Python test_new_indicators.py cover all eight new indicators (Node 91/91, Python 117/117 green). cargo fmt + clippy (core/wickra/data/wasm/node) clean; 508 core tests, 25 data tests and 74 doctests green.
4.6 KiB
PPO
Percentage Price Oscillator — MACD expressed as a percentage of the slow EMA, so readings are comparable across instruments.
Quick reference
| Field | Value |
|---|---|
| Family | Price Oscillators |
| Input type | f64 (single close) |
| Output type | f64 |
| Output range | unbounded around zero (percent) |
| Default parameters | (fast = 12, slow = 26) (Python) |
| Warmup period | slow |
| Interpretation | Percentage gap between a fast and slow EMA; zero-line crosses are signals. |
Formula
PPO = 100 · (EMA_fast − EMA_slow) / EMA_slow
PPO is MacdIndicator divided by the slow
EMA. That single change makes it scale-free: a PPO of 1.5 always
means "the fast EMA is 1.5 % above the slow EMA", whether the instrument
trades at $5 or $5000 — so PPO values can be compared across assets and
across time, which raw MACD values cannot. The classic PPO signal
line is a 9-period EMA of this PPO line; compose it with
Chain and an Ema(9).
Parameters
| Name | Type | Default | Valid range | Description |
|---|---|---|---|---|
fast |
usize |
12 (Python) |
>= 1, < slow |
Fast EMA period. |
slow |
usize |
26 (Python) |
> fast |
Slow EMA period. |
fast must be strictly less than slow — otherwise new returns
Error::InvalidPeriod. A zero period returns Error::PeriodZero. The
Python binding defaults the pair to (12, 26); the periods property
returns (fast, slow).
Inputs / Outputs
From crates/wickra-core/src/indicators/ppo.rs:
impl Indicator for Ppo {
type Input = f64;
type Output = f64;
// update(&mut self, input: f64) -> Option<f64>
}
A single f64 close in, an Option<f64> out. Python maps this to
float | None / numpy.ndarray (NaN warmup); Node to number | null /
Array<number> (NaN warmup).
Warmup
Ppo::new(fast, slow).warmup_period() == slow. Both EMAs are SMA-seeded;
the slow EMA is the last to seed, at input slow, which is also when PPO
emits its first value.
Edge cases
- Constant series. Both EMAs converge to the constant, so their gap —
and PPO — is
0(constant_series_yields_zeropins this). - Zero slow EMA. A
0.0slow EMA would divide by zero; PPO reports0.0for that bar instead. - NaN / infinity inputs. Non-finite inputs are silently dropped; the EMAs are not advanced.
- Reset.
ppo.reset()clears both EMAs and the cached value.
Examples
Rust
use wickra::{BatchExt, Indicator, Ppo};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut ppo = Ppo::new(12, 26)?;
let prices: Vec<f64> = (1..=80).map(f64::from).collect();
let out = ppo.batch(&prices);
println!("warmup_period = {}", ppo.warmup_period());
println!("last > 0: {}", out.last().unwrap().unwrap() > 0.0);
Ok(())
}
Output:
warmup_period = 26
last > 0: true
In a rising series the fast EMA leads the slow EMA, so PPO is positive.
Python
import numpy as np
import wickra as ta
ppo = ta.PPO() # (fast=12, slow=26)
prices = np.full(60, 100.0) # flat series
print(ppo.batch(prices)[-1]) # both EMAs equal -> 0
Output:
0.0
Node
const ta = require('wickra');
const ppo = new ta.PPO(12, 26);
const prices = Array.from({ length: 80 }, (_, i) => 100 + i);
console.log('warmupPeriod:', ppo.warmupPeriod());
Interpretation
Ppo is read exactly like MACD: the zero-line cross (fast EMA crossing
the slow EMA), the signal-line cross (PPO crossing its own 9-EMA), and
histogram-style divergence. Its advantage over MACD is comparability — a
PPO scan across a watchlist ranks instruments by relative trend
strength, which a MACD scan cannot do because MACD is in each
instrument's own price units.
Common pitfalls
- Expecting a bundled signal line.
Ppohere is the single PPO line; addEma(9)viaChainfor the signal line and histogram. fast >= slow. The constructor rejects it — the fast EMA must be the faster one.
References
Gerald Appel's MACD, re-expressed as a percentage. The implementation
follows the standard PPO definition and matches TA-Lib's PPO.
See also
- Indicator-MacdIndicator.md — the price-unit original, with a bundled signal line and histogram.
- Indicator-Ema.md — the underlying average.
- Indicators-Overview.md — the full taxonomy.