PriceOscillator — Price Oscillator
A fast simple moving average minus a slow one. Positive means the short-term average leads the long-term average; negative means it trails.
PriceOscillator is the identifier a strategy writes; Price Oscillator is the label the chart’s indicator picker shows. Single value — .value is the only accessor.
Signature
PriceOscillator(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. |
Both periods are positional and in that order — PriceOscillator(12, 26, on=self.trade).
Formula
$$ \text{PO}t = \frac{1}{F} \sum{i=0}^{F-1} \text{close}{t-i} ;-; \frac{1}{S} \sum{i=0}^{S-1} \text{close}_{t-i} $$
Two plain SMAs of the same source, subtracted. The result is in the source’s units (normally price), so it is unbounded and scales with the instrument — a reading of 1.40 means very different things 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 before the indicator reports. Until then it returns 0, not a partial difference.
Two arguments, one warmup number
A strategy’s history prefill is sized from each indicator’s first argument (fast_period here) plus one bar. The slow buffer needs more than that, and execute() additionally waits until every indicator’s own window is full — so nothing is ever computed on a half-filled window; the run simply starts a little later than the prefill alone suggests. Give the range enough room ahead of the bars you care about.
Reading the value
po.value # current value, in price unitspo.value_at(1) # previous bar's valuepo > 0 # comparison reads .value on both sidespo.crossed_above(0) # the bar the fast average takes the leadpo.crossed_below(0)Example
class OscillatorTrend(Strategy): """Hold while the fast average leads the slow one.""" fast = Param(12, min=2, max=50) slow = Param(26, min=10, max=200) trade = Symbol() sizing = FixedCapital()
def execute(self): po = PriceOscillator(self.fast, self.slow, on=self.trade) self.stop_loss(2.0)
if po.crossed_above(0) and self.trade.is_flat: self.buy(self.trade, reason=f"PO turned positive ({po.value:.2f})")
if po.crossed_below(0) and self.trade.is_long: self.sell(self.trade, self.trade.position, reason="PO turned negative")Notes
- The zero line is the whole signal:
PO > 0is exactlyfast SMA > slow SMA. If that is all you need, anSMAcrossover says the same thing in a form other readers recognize instantly. - Because it is unbounded and price-scaled, a fixed threshold like
po.value > 2is not portable across instruments. Use the zero cross, or switch toPercentPriceOscillator, which divides the same difference by the slow average. fast_periodlarger thanslow_periodis accepted — the buffers do not care which is which. It simply inverts the sign of every reading, which is almost never what you meant.- This is the MACD line without the signal EMA. If you want the smoothing and the histogram, use
MACD.