The displacement is the whole point: subtracting the current average
leaves a series that still lags the trend, while subtracting an average
from half a window ago removes the trend the average was measuring and
leaves the cycle. DPO is used to read cycle length, not direction, and
it deliberately does not extend to the last bar of a trend the way a plain
price − MA does.
The integer rule on odd periods
period/2 + 1 is not an integer for odd period, and the published
sources write it without saying which way it goes. We floor:
shift = ⌊period/2⌋ + 1, so period 20 → 11 and period 15 → 8. That is
StockCharts' {X/2 + 1} read as the number of whole bars back, and it is
the only rule that keeps the study a pure re-indexing of the average (the
alternative is interpolating between two bars of it, which no
implementation does). The kernel makes the same call for zlema's lag, for
the same reason.
Which alignment (a real fork, named)
There are two published DPOs and they are different series, not one
series plotted two ways:
Ours (TradingView's default, isCentered = false):
price[i] − MA[i − shift] — the current bar's price against a displaced
average.
The centered form (StockCharts, TradingView's isCentered = true):
price[i − shift] − MA[i] — a past price against the current average.
Both are causal at bar i; they differ in which bar the value describes.
We ship the first because a study here appends a column aligned to the
source's own time axis: the value on bar i has to be about bar i's
price, or it silently misaligns with everything else on that row (the
bar's close, another study's output, a chart's crosshair). The centered
form's value on bar i is about a price shift bars earlier, which is a
plotting convention, not a column. A caller who wants it can shift ours —
the numbers are not the same series, so we do not claim they are.
maType is offered because the average is the only free choice in the
definition and the K2 engine already names the menu; the classic DPO is
the SMA one, which is the default.
Definition source
TA-Lib has no DPO, so the oracle is a pandas replication with the
analytic first-valid bar asserted (below), not a vendor mask copied.
Edges
Warm-up is the average's, plus the displacement: first value on bar
period − 1 + shift for the window types and ema (bar 30 at the
default period 20), later for dema / tema / hull / kama /
zlema, whose own first valid bar is later. Length-preserving; earlier
rows undefined.
Linear in the input, and shift-invariant — a difference of prices, so
scaling every price by k scales the reading by k (the macd
side of the scale pair, not the disparityIndex side), while
adding a constant adds it to both the price and the displaced average
and cancels. Both pinned by a property test, over every maType, since
every type in the menu satisfies MA(a·x + b) = a·MA(x) + b.
An interior gap costs the bar itself (the price is missing) and
the bar shift later (the displaced average is missing there), plus
whatever the maType's own gap rule costs — window types recover, the
ema family skips, smma and kama propagate to the end.
A series shorter than period − 1 + shift reads entirely
undefined, with the row count kept.
Detrended Price Oscillator (DPO) — the price less a moving average displaced
shift = ⌊period/2⌋ + 1bars back:The displacement is the whole point: subtracting the current average leaves a series that still lags the trend, while subtracting an average from half a window ago removes the trend the average was measuring and leaves the cycle. DPO is used to read cycle length, not direction, and it deliberately does not extend to the last bar of a trend the way a plain
price − MAdoes.The integer rule on odd periods
period/2 + 1is not an integer for oddperiod, and the published sources write it without saying which way it goes. We floor:shift = ⌊period/2⌋ + 1, soperiod 20 → 11andperiod 15 → 8. That is StockCharts'{X/2 + 1}read as the number of whole bars back, and it is the only rule that keeps the study a pure re-indexing of the average (the alternative is interpolating between two bars of it, which no implementation does). The kernel makes the same call forzlema's lag, for the same reason.Which alignment (a real fork, named)
There are two published DPOs and they are different series, not one series plotted two ways:
isCentered = false):price[i] − MA[i − shift]— the current bar's price against a displaced average.isCentered = true):price[i − shift] − MA[i]— a past price against the current average.Both are causal at bar
i; they differ in which bar the value describes. We ship the first because a study here appends a column aligned to the source's own time axis: the value on barihas to be about bari's price, or it silently misaligns with everything else on that row (the bar's close, another study's output, a chart's crosshair). The centered form's value on bariis about a priceshiftbars earlier, which is a plotting convention, not a column. A caller who wants it can shift ours — the numbers are not the same series, so we do not claim they are.maTypeis offered because the average is the only free choice in the definition and the K2 engine already names the menu; the classic DPO is the SMA one, which is the default.Definition source
TA-Lib has no DPO, so the oracle is a pandas replication with the analytic first-valid bar asserted (below), not a vendor mask copied.
Edges
period − 1 + shiftfor the window types andema(bar 30 at the defaultperiod 20), later fordema/tema/hull/kama/zlema, whose own first valid bar is later. Length-preserving; earlier rowsundefined.kscales the reading byk(the macd side of the scale pair, not the disparityIndex side), while adding a constant adds it to both the price and the displaced average and cancels. Both pinned by a property test, over everymaType, since every type in the menu satisfiesMA(a·x + b) = a·MA(x) + b.shiftlater (the displaced average is missing there), plus whatever themaType's own gap rule costs — window types recover, theemafamily skips,smmaandkamapropagate to the end.period − 1 + shiftreads entirelyundefined, with the row count kept.