Skip to content
Open RaamTrade

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 units
po.value_at(1) # previous bar's value
po > 0 # comparison reads .value on both sides
po.crossed_above(0) # the bar the fast average takes the lead
po.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 > 0 is exactly fast SMA > slow SMA. If that is all you need, an SMA crossover says the same thing in a form other readers recognize instantly.
  • Because it is unbounded and price-scaled, a fixed threshold like po.value > 2 is not portable across instruments. Use the zero cross, or switch to PercentPriceOscillator, which divides the same difference by the slow average.
  • fast_period larger than slow_period is 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.