Detrended Price Oscillator (DPO)
I. What is the DPO Indicator
The Detrended Price Oscillator (DPO) is an oscillator that removes the long-term trend component from prices to reveal short-term cycles. It calculates the deviation between the closing price and a backward-displaced moving average, stripping away the trend influence so that traders can more clearly see the cyclical fluctuation patterns in price.
Historical Background
DPO does not have a single clearly identified inventor. It was developed by the technical analysis community during research on Market Cycles. Its theoretical foundation comes from Cycle Analysis, which assumes that price movements can be decomposed into a trend component and a cyclical component. By removing the trend, DPO helps traders identify underlying cyclical patterns.
Indicator Classification
- Type: Oscillator, displayed in a separate panel
- Category: Momentum / Cycle indicator
- Default Parameter: Period
- Value Range: Unbounded, oscillates around the zero line
DPO is not concerned with “whether price is rising or falling” (that’s the job of trend indicators). Instead, it asks: “Where is price within its cycle?” — is it at a cycle high or low?
II. Mathematical Principles and Calculation
Core Formula
Where:
- = current closing price
- = -period Simple Moving Average
- Displacement = (shifted into the past)
For the default : Displacement =
Therefore:
Equivalent Understanding
The DPO calculation can be understood as follows:
- Calculate the 20-period SMA
- Shift the SMA right by 11 periods (i.e., use the SMA value from 11 periods ago)
- Subtract this displaced SMA from the current price
Step-by-Step Calculation Logic
- Calculate SMA(Close, 20)
- Determine displacement:
- Displace the SMA: Shift the SMA series right by 11 periods
- DPO = Close - displaced SMA
Why the Displacement?
The center point of SMA(20) falls at the middle of its window (approximately 10 periods ago). By shifting the SMA right by periods, we realign it to the center of the window, making DPO more accurately reflect the price’s deviation around the moving average.
The “detrended” in DPO means removing the trend component represented by the SMA. When DPO > 0, the price is above the detrended baseline; when DPO < 0, the price is below it. Due to the displacement, DPO does not contain the most recent trend information, so it is not suitable for real-time trend tracking.
III. Python Implementation
import numpy as np
import pandas as pd
def dpo(close: pd.Series, period: int = 20) -> pd.Series:
"""
Calculate Detrended Price Oscillator (DPO)
Parameters
----------
close : pd.Series
Close price series
period : int
Calculation period, default 20
Returns
-------
pd.Series
DPO value series
"""
shift_amount = period // 2 + 1
sma = close.rolling(window=period, min_periods=period).mean()
# Displace SMA to the right (use SMA from shift_amount periods ago)
dpo_values = close - sma.shift(shift_amount)
return dpo_values
# ========== Usage Example ==========
if __name__ == "__main__":
np.random.seed(42)
dates = pd.date_range("2024-01-01", periods=150, freq="D")
# Construct trend + cyclical price
trend = np.linspace(0, 15, 150)
cycle = 5 * np.sin(2 * np.pi * np.arange(150) / 25) # 25-day cycle
noise = np.random.randn(150) * 0.5
price = 100 + trend + cycle + noise
df = pd.DataFrame({
"date": dates,
"open": price + np.random.randn(150) * 0.3,
"high": price + np.abs(np.random.randn(150) * 0.6),
"low": price - np.abs(np.random.randn(150) * 0.6),
"close": price,
"volume": np.random.randint(1000, 10000, size=150),
})
df.set_index("date", inplace=True)
# Calculate DPO
df["DPO_20"] = dpo(df["close"], period=20)
print("=== DPO Results (Last 20 Rows) ===")
print(df[["close", "DPO_20"]].tail(20).to_string())
# Zero-line crossover signals
df["cross_up"] = (df["DPO_20"] > 0) & (df["DPO_20"].shift(1) <= 0)
df["cross_down"] = (df["DPO_20"] < 0) & (df["DPO_20"].shift(1) >= 0)
print("\n=== DPO Crosses Above Zero ===")
print(df[df["cross_up"]][["close", "DPO_20"]].to_string())
print("\n=== DPO Crosses Below Zero ===")
print(df[df["cross_down"]][["close", "DPO_20"]].to_string())
# Cycle highs and lows
from scipy.signal import argrelextrema # Optional dependency
try:
dpo_clean = df["DPO_20"].dropna()
highs = argrelextrema(dpo_clean.values, np.greater, order=5)[0]
lows = argrelextrema(dpo_clean.values, np.less, order=5)[0]
print("\n=== Cycle Highs ===")
print(dpo_clean.iloc[highs].to_string())
print("\n=== Cycle Lows ===")
print(dpo_clean.iloc[lows].to_string())
if len(highs) >= 2:
avg_cycle = np.mean(np.diff(highs))
print(f"\nEstimated cycle length: approx. {avg_cycle:.1f} days")
except ImportError:
print("\n(Install scipy to auto-detect cycle highs and lows)")
IV. Problems the Indicator Solves
1. Cycle Identification
DPO’s primary use is identifying cyclical patterns in price:
- The distance between DPO peaks = the dominant cycle length in price
- By measuring the spacing between multiple peaks or troughs, you can estimate the market’s underlying cycle
2. Overbought / Oversold Within Cycles
Once a cycle is identified, DPO can be used to determine where the current price sits within the cycle:
- DPO near cycle high -> cyclical overbought, likely to pull back
- DPO near cycle low -> cyclical oversold, likely to bounce
3. Short-term Timing Within Trends
Even during an uptrend, prices experience short-term pullbacks. DPO helps identify the bottom of these pullbacks:
- In an uptrend, DPO falling to negative then bouncing back -> buy opportunity
- In a downtrend, DPO rising to positive then falling back -> sell opportunity
4. Pairing with Cycle Analysis Tools
DPO is often used alongside Fourier analysis, spectral analysis, and other mathematical tools to extract the market’s dominant cycles more precisely.
Due to the SMA displacement, the last few DPO values (approximately periods) are missing or unreliable. This means DPO is not suitable as the sole basis for real-time trading signals — it is better suited for analyzing cycle patterns and providing supplementary judgment.
V. Advantages, Disadvantages, and Use Cases
Advantages
| Advantage | Description |
|---|---|
| Removes trend | Eliminates trend interference, clearly revealing cyclical fluctuations |
| Cycle measurement | Can be used to estimate the market’s dominant cycle length |
| Intuitive | Zero-line crossovers and peak/trough patterns are easy to understand |
| Complementary perspective | Provides a cycle dimension that other trend/momentum indicators lack |
Disadvantages
| Disadvantage | Description |
|---|---|
| Missing data | Displacement causes the most recent periods to be uncalculable |
| Not real-time | Not suitable for real-time trading decisions; signals have inherent delay |
| Cycle assumption | Assumes stable cycles exist in price, but actual market cycles may be unstable |
| Parameter selection difficulty | Need to approximately know the cycle length before choosing appropriate parameters |
Use Cases
- Commodity futures: Many commodities (e.g., agricultural products) have seasonal cycles that DPO captures well
- Stock indices: Used in the context of known cycles such as election cycles or earnings seasons
- Paired with cycle analysis: As part of a cycle analysis toolkit
Comparison with Related Indicators
| Comparison | DPO | CCI | RSI |
|---|---|---|---|
| Detrended | Yes | No | No |
| Real-time | Poor | Good | Good |
| Cycle analysis | Specialty | General | Not suited |
| Value range | Unbounded | Unbounded | 0–100 |
- Determine the cycle first: Before using DPO, estimate the market’s dominant cycle length through visual observation or mathematical methods (such as autocorrelation functions).
- Match parameters: DPO’s period parameter should be set to half the estimated cycle length or equal to it.
- Combine with trend direction: In an uptrend, only focus on DPO lows for buying; in a downtrend, only focus on DPO highs for selling.
- Don’t rely on recent values: Due to displacement, the last periods of DPO values are incomplete — use other indicators to compensate.