PercentPriceOscillator — Percent Price Oscillator
תוכן זה אינו זמין עדיין בשפה שלך.
The Price Oscillator expressed as a percentage of the slow moving average. Same signal, but comparable across instruments and across time.
PercentPriceOscillator is the identifier a strategy writes; Percent Price Oscillator is the label the chart’s indicator picker shows. Single value — .value is the only accessor.
Signature
PercentPriceOscillator(fast_period, slow_period, on=<source>)| Argument | Type | Required | Notes |
|---|---|---|---|
fast_period |
int Param or literal | Yes | Short window. Default 12, bounds 2–50. |
slow_period |
int Param or literal | Yes | Long window. Default 26, bounds 10–200. |
on= |
symbol, OHLCV field, or indicator | No | Source. Defaults to the trade symbol’s close. |
Formula
$$ \text{PPO}_t = \frac{\text{SMA}_F - \text{SMA}_S}{\text{SMA}_S} \times 100 $$
The numerator is the plain Price Oscillator; dividing by the slow average turns price units into percent. A reading of 1.5 means the fast average sits 1.5% above the slow one — the same statement on a $12 stock and a $600 one.
Warmup
max(fast_period, slow_period) bars — in practice slow_period, since both buffers must be full. Until then it returns 0.
Two arguments, one warmup number
History prefill is sized from the first argument (fast_period) plus one bar, and execute() additionally waits until every indicator’s own window is full. Nothing is computed on a half-filled window; the run just starts later than the prefill number alone suggests.
Reading the value
ppo.value # percent, e.g. 1.5 means +1.5%ppo.value_at(1)ppo > 1.5 # a threshold that travels between instrumentsppo.crossed_above(0)Example
class PercentOscillatorTrend(Strategy): """Enter only when the fast/slow gap is a meaningful percentage.""" fast = Param(12, min=2, max=50) slow = Param(26, min=10, max=200) threshold = Param(1, min=1, max=10) trade = Symbol() sizing = FixedCapital()
def execute(self): ppo = PercentPriceOscillator(self.fast, self.slow, on=self.trade) self.stop_loss(2.0)
if ppo.value > self.threshold and self.trade.is_flat: self.buy(self.trade, reason=f"PPO {ppo.value:.2f}%")
if ppo.value < 0 and self.trade.is_long: self.sell(self.trade, self.trade.position, reason="PPO back below zero")Notes
- This is the version to optimize over. A percentage threshold found on one symbol is at least meaningful on another; the raw
PriceOscillatorthreshold is not. - The division is not floored. The seeded source divides by the slow SMA directly, with no epsilon guard. On price that is harmless — a slow average of exactly zero cannot happen. Chained onto something that oscillates through zero (
PercentPriceOscillator(12, 26, on=some_oscillator)), it can, and there is no protective clamp. Chain onto price-like sources. - The value is already in percent — do not multiply by 100 again.
- Zero crossings are identical to the Price Oscillator’s, since the denominator is positive for price sources. What changes is only the magnitude scale, which is where the usefulness lives.