William Blau formalized the Detrended Price Oscillator in *Momentum, Direction, and Divergence* (1995), though the concept of removing trend via displaced moving averages predates his work by decades. The core insight is straightforward: if you shift an SMA backward by half its period, the average sits at the center of the window rather than its trailing edge. Subtracting that centered value from price strips the trend component and exposes the underlying cycle.
Most oscillators try to be current. DPO deliberately looks backward. That design choice makes it useless for real-time trend-following but surprisingly effective for cycle analysis. Traders who need to estimate the dominant period of a market's oscillation find DPO more honest than indicators that blend trend and cycle information into a single, ambiguous line.
The PineScript (non-centered) variant used here computes `close - SMA[displacement]`, referencing the SMA from `displacement` bars ago rather than centering the SMA around the current bar. This differs from the Tulip/TA-Lib centered variant (`close[displacement] - SMA`), which anchors the price to the past instead. Both isolate cycles, but they produce different numeric results and are not cross-comparable.
DPO measures the deviation of current price from a displaced simple moving average. By pushing the SMA backward in time, the trend component cancels out, leaving only the cyclical oscillation around zero.
The result is an unbounded oscillator that crosses zero when price equals its detrended average. Peaks and troughs in the DPO correspond to short-term cycle extremes. The distance between consecutive peaks (or troughs) estimates the dominant cycle period. This makes DPO a cycle-measurement tool rather than a trading signal generator.
Because DPO is not aligned to the latest bar, it should not be used for real-time entry/exit decisions. Its value lies in structural analysis: identifying how frequently a market oscillates and whether the current cycle amplitude is expanding or contracting.
The SMA requires $P$ bars to fill. The displacement buffer then requires $\lfloor P/2 \rfloor + 1$ additional bars before the oldest SMA value is available.
Two `RingBuffer` instances partition the computation:
-`_smaBuffer(period)`: maintains a running sum for O(1) SMA calculation via `Sum / period`.
-`_smaHistory(displacement + 1)`: stores computed SMA values; `.Oldest` retrieves the displaced SMA.
### 2. Source Subscription
The chaining constructor subscribes to `src.Pub += Handle`, forwarding `TValueEventArgs` directly into `Update` without storing the source reference.
### 3. Batch Path
`Batch(ReadOnlySpan<double>, Span<double>, int)` mirrors the streaming logic with local `RingBuffer` instances. No SIMD acceleration is applied because the displacement dependency creates a sequential data-flow constraint.
### 4. Edge Cases
| Condition | Behavior |
|-----------|----------|
| `period <= 0` | `ArgumentException` with `nameof(period)` |
| `NaN` / `Infinity` input | Substitutes last valid value |
| Before warmup complete | Returns `0.0` |
| `isNew = false` | `Restore()` on both RingBuffers; recomputes with corrected value |
## Interpretation and Signals
### Signal Zones
| Zone | Condition | Interpretation |
|------|-----------|----------------|
| Above zero | `DPO > 0` | Price above detrended average |
| Below zero | `DPO < 0` | Price below detrended average |
| Peak | Local maximum | Short-term cycle top |
| Trough | Local minimum | Short-term cycle bottom |
### Signal Patterns
- **Zero-line crossover**: DPO crossing above zero suggests price has moved above its displaced average; crossing below suggests the opposite.
- **Peak-to-peak measurement**: Distance between consecutive DPO peaks estimates the dominant cycle length.
| Reason | Displacement creates sequential dependency between SMA output and DPO computation |
| Fallback | Scalar loop with local `RingBuffer` instances |
## Common Pitfalls
1.**Confusing DPO variants**: The PineScript (non-centered) and Tulip/TA-Lib (centered) formulas produce different results. Cross-library validation requires matching the same variant.
2.**Using DPO for real-time signals**: DPO is displaced by design. It does not reflect the latest bar's relationship to the current trend.
3.**Wrong period selection**: If the DPO period does not approximate the dominant cycle, the oscillator produces noise rather than signal. Measure peak-to-peak distance to calibrate.
4.**Ignoring warmup**: The first `period + displacement` bars output `0.0`. Trading on these values guarantees false signals.
5.**Trend-dominated markets**: In strong trends, DPO stays persistently positive or negative, defeating its cycle-isolation purpose.
## FAQ
**Q: Why does QuanTAlib's DPO not match Tulip or TA-Lib?**
A: Different formula variant. QuanTAlib uses the PineScript non-centered formula (`close - SMA[displacement]`), while Tulip and TA-Lib use the centered variant (`close[displacement] - SMA`). Both are valid but not numerically equivalent.
**Q: How do I pick the right period?**
A: Start with a period that matches your expected cycle length (e.g., 20 for daily data targeting a roughly monthly cycle). Then measure peak-to-peak distance in the DPO output. If the measured distance consistently differs from your period, adjust accordingly.
**Q: Can DPO be used as a standalone trading signal?**
A: Not recommended. DPO measures cycle structure, not timing. Pair it with a trend-following indicator (SMA, EMA) for entries and use DPO to confirm whether the market is in a cyclical phase worth trading.