Price Oscillator — the spread between a fast and a slow moving average
of one column, either as a percent of the slow average (mode: 'percent', the default) or in the source's own units (mode: 'absolute'):
Both averages come from the shared K2 engine, so maType is the whole
MaType menu rather than a private pair of smoothers.
Why mode defaults to 'percent'
TA-Lib ships the two forms as two functions — APO (absolute) and
PPO (percent) — over identical inputs, so the name alone does not settle
a default. Two things do:
The absolute form at the default parameters is already shipped.priceOscillator({ mode: 'absolute', maType: 'ema', fastPeriod: 12, slowPeriod: 26 }) is macd's macdLine, bar for bar — a test
pins that identity. Defaulting to it would make the headline call of a
new study a rename of an existing column, which is what the studies
README's step 0 exists to catch. Defaulting to 'percent' makes the
default call the thing MACD cannot give you.
The percent form is the comparable one. It is scale-invariant
(a property test pins it), so a reading of 1.8 means the same thing on
a $4 stock and a $4,000 one; the absolute form is in price units and is
linear in them (pinned too).
The knob is a mode string rather than a percent: boolean so the two
settings read as what they are at the call site, and rather than two
exported functions (apo / ppo) because one study with one knob is the
shape stochastic already set with slowing: 1 for the fast
stochastic.
Definition and TA-Lib
TA-Lib's APO/PPO(matype) are exactly MA(fast) − MA(slow) and
100·(MA(fast) − MA(slow))/MA(slow) over the same MA(matype) — verified
in the oracle generator, which rebuilds both from talib.MA and asserts
they agree to 8.9e-16 before using them as the reference.
On the types TA-Lib ships that are not in the EMA family, we match it
outright: { maType: 'sma', fastPeriod: 5, slowPeriod: 13, mode: 'absolute' } agrees with APO(matype=0) to 7.1e-14 with an identical
warm-up mask (oracle case).
On 'ema' (and dema/tema) the macd seed precedent applies:
pond's EMA is seeded on the first sample, TA-Lib's on the SMA of the first
n, so the values differ by a decaying transient. Reseeding here would
make priceOscillator({ maType: 'ema' }) disagree with ema() and with
macd() inside this package — a worse surprise than a sub-percent
divergence from a vendor whose bar-for-bar parity is an explicit non-goal.
The generator therefore splits the check the way the K2 engine's does:
the formula is rebuilt on TA-Lib's own SMA seed and required to match
PPO/APO exactly (matched to 2.8e-14), and the seed transient is
bounded separately over the last 20 shared bars. Measured on the oracle
input at {12, 26, ema, percent}: 6.61% of scale at the first shared
bar, 0.41% at its worst over the last 20 (0.089% at the last bar), masks
identical. The bound discriminates: the same replication run on a wrong
rate is 5.09% (2/(n+2)), 5.99% (2/n) or 44.4% (1/n) over those same
20 bars.
Edges
Warm-up is the slower average's, per the K2 engine's table — bar
slowPeriod − 1 for the window types and ema, later for dema /
tema / hull / kama / zlema. Length-preserving; earlier rows are
undefined.
A leading gap shifts the start for every maType except 'sma',
which keeps sma()'s row-counting window (the column door's documented
asymmetry). Running over another study's output therefore starts late
rather than coming back empty.
An interior gap costs whatever the chosen maType costs — window
types recover once it leaves the window, the ema family skips the bar,
smma and kama propagate to the end. Stated per type on
movingAverageValues, not averaged into a slogan.
A zero slow average (only reachable on a column that can be zero or
negative — a return series, another oscillator) makes the percent form a
0/0 or a division by zero. It reports no value, not Infinity and
not 0: the ratio is genuinely undefined there, and a ±Infinity
reaching a chart's y-domain is worse than a gap. The absolute form has no
such case and keeps reporting the spread.
fastPeriod must be shorter than slowPeriod — a caller who swaps
them wants the negated series, and silently obliging would make the sign
of every reading meaningless. Throws, like macd.
Price Oscillator — the spread between a fast and a slow moving average of one column, either as a percent of the slow average (
mode: 'percent', the default) or in the source's own units (mode: 'absolute'):Both averages come from the shared K2 engine, so
maTypeis the whole MaType menu rather than a private pair of smoothers.Why
modedefaults to'percent'TA-Lib ships the two forms as two functions —
APO(absolute) andPPO(percent) — over identical inputs, so the name alone does not settle a default. Two things do:priceOscillator({ mode: 'absolute', maType: 'ema', fastPeriod: 12, slowPeriod: 26 })is macd'smacdLine, bar for bar — a test pins that identity. Defaulting to it would make the headline call of a new study a rename of an existing column, which is what the studies README's step 0 exists to catch. Defaulting to'percent'makes the default call the thing MACD cannot give you.1.8means the same thing on a $4 stock and a $4,000 one; the absolute form is in price units and is linear in them (pinned too).The knob is a
modestring rather than apercent: booleanso the two settings read as what they are at the call site, and rather than two exported functions (apo/ppo) because one study with one knob is the shape stochastic already set withslowing: 1for the fast stochastic.Definition and TA-Lib
TA-Lib's
APO/PPO(matype)are exactlyMA(fast) − MA(slow)and100·(MA(fast) − MA(slow))/MA(slow)over the sameMA(matype)— verified in the oracle generator, which rebuilds both fromtalib.MAand asserts they agree to8.9e-16before using them as the reference.On the types TA-Lib ships that are not in the EMA family, we match it outright:
{ maType: 'sma', fastPeriod: 5, slowPeriod: 13, mode: 'absolute' }agrees withAPO(matype=0)to7.1e-14with an identical warm-up mask (oracle case).On
'ema'(anddema/tema) the macd seed precedent applies: pond's EMA is seeded on the first sample, TA-Lib's on the SMA of the firstn, so the values differ by a decaying transient. Reseeding here would makepriceOscillator({ maType: 'ema' })disagree withema()and withmacd()inside this package — a worse surprise than a sub-percent divergence from a vendor whose bar-for-bar parity is an explicit non-goal. The generator therefore splits the check the way the K2 engine's does: the formula is rebuilt on TA-Lib's own SMA seed and required to matchPPO/APOexactly (matched to2.8e-14), and the seed transient is bounded separately over the last 20 shared bars. Measured on the oracle input at{12, 26, ema, percent}: 6.61% of scale at the first shared bar, 0.41% at its worst over the last 20 (0.089% at the last bar), masks identical. The bound discriminates: the same replication run on a wrong rate is 5.09% (2/(n+2)), 5.99% (2/n) or 44.4% (1/n) over those same 20 bars.Edges
slowPeriod − 1for the window types andema, later fordema/tema/hull/kama/zlema. Length-preserving; earlier rows areundefined.maTypeexcept'sma', which keepssma()'s row-counting window (the column door's documented asymmetry). Running over another study's output therefore starts late rather than coming back empty.maTypecosts — window types recover once it leaves the window, theemafamily skips the bar,smmaandkamapropagate to the end. Stated per type on movingAverageValues, not averaged into a slogan.0/0or a division by zero. It reports no value, notInfinityand not0: the ratio is genuinely undefined there, and a±Infinityreaching a chart's y-domain is worse than a gap. The absolute form has no such case and keeps reporting the spread.fastPeriodmust be shorter thanslowPeriod— a caller who swaps them wants the negated series, and silently obliging would make the sign of every reading meaningless. Throws, like macd.