Detrended Price Oscillator on TradingView: formula and limits
The Detrended Price Oscillator indicator compares price with a displaced SMA to study cycles. Its centered plot appears on earlier bars, so the location of a peak does not tell you when that peak became knowable. Check the formula and calculation time before using DPO in alerts or a strategy.
Two timestamps for every centered point
With N = 21, the shift is floor(21 / 2) + 1 = 11 bars. A research value calculated at bar 100 is drawn at bar 89. It cannot justify a trade at bar 89.
Formula, displacement and display position
Let C[t] be the close and M[t] the N-bar SMA ending at bar t. This example sets s = floor(N / 2) + 1 explicitly. For both N = 20 and N = 21, s is 11. Pine v6 division can return a fraction; rounding down before creating an integer history index avoids an ambiguous half-bar shift.
| Series | Calculated at t | Displayed at | Earliest final value |
|---|---|---|---|
| Non-centered variant | C[t] − M[t−s] | t (offset 0) | Close of t |
| Centered research | C[t−s] − M[t] | t−s (offset −s) | Close of t |
The research row follows TradingView's published historical-price-minus-current-SMA formula. The non-centered row retains this page's current-price-minus-older-SMA calculation. They need not have the same sign: switching formulas is more than moving one line. When comparing a built-in or community indicator, inspect its source and Centered setting before expecting matching values.
In Pine, ma[s] retrieves an older SMA. Separately, offset=-s draws a calculated point s bars to the left. At its displayed bar u, the research point equals C[u] − M[u+s]. That later SMA is the reason the plotted point was unavailable at u.
References: DPO calculation, plot offsets and v6 division.
An eight-bar calculation you can reproduce
Assume eight consecutive closed bars with closes 10, 12, 14, 16, 14, 12, 10, 8. Use N = 4, so s = 3. Bars are numbered from 1. These are invented prices for arithmetic, with no gaps and no trade results.
| Calculation bar t | Close | SMA(4) | Older SMA[t−3] | Non-centered | Research value → plot bar |
|---|---|---|---|---|---|
| 4 | 16 | 13 | Unavailable | Unavailable | −3 → bar 1 |
| 5 | 14 | 14 | Unavailable | Unavailable | −2 → bar 2 |
| 6 | 12 | 14 | Unavailable | Unavailable | 0 → bar 3 |
| 7 | 10 | 13 | 13 | −3 | +3 → bar 4 |
| 8 | 8 | 11 | 14 | −6 | +3 → bar 5 |
At bar 7, M[7] = (16 + 14 + 12 + 10) / 4 = 13. The research result is C[4] − M[7] = 16 − 13 = +3, placed on bar 4. Meanwhile, the non-centered result is C[7] − M[4] = 10 − 13 = −3. The opposite signs come from different inputs.
The centered series crosses from 0 to +3 on calculation bar 7 but draws that positive value at bar 4. Using that crossing to enter on bar 4 would use closes from bars 5, 6 and 7. At bar 8, the last research point is at bar 5; bars 6 through 8 remain blank on the centered plot.
Pine Script with separate research and alert paths
Paste this indicator into TradingView's Pine Editor and add it to the chart. The blue non-centered line is on by default. Enable the orange research plot only when inspecting past cycles. Data Window fields expose both SMA inputs and the research value at its actual calculation bar.
//@version=6
indicator("DPO: calculation time and centered research", overlay=false)
length = input.int(21, "SMA length", minval=2, maxval=500)
showCentered = input.bool(false, "Show centered research plot (backdated)")
shift = int(math.floor(length / 2.0)) + 1
ma = ta.sma(close, length)
// Current close minus an older SMA: available on the calculation bar.
liveDpo = close - ma[shift]
// Historical close minus the current SMA: only known on the calculation bar.
researchDpo = close[shift] - ma
plot(liveDpo, "Non-centered DPO", color=color.blue, linewidth=2)
plot(showCentered and barstate.isconfirmed ? researchDpo : na, "Centered research only", color=color.orange, offset=-shift, style=plot.style_linebr)
hline(0, "Zero", color=color.gray)
plot(ma, "Current SMA", display=display.data_window)
plot(ma[shift], "Historical SMA", display=display.data_window)
plot(researchDpo, "Research value on calculation bar", display=display.data_window)
// Alerts and markers use only the non-centered series, with no plot offset.
crossUp = ta.crossover(liveDpo, 0)
crossDown = ta.crossunder(liveDpo, 0)
confirmedUp = barstate.isconfirmed and crossUp
confirmedDown = barstate.isconfirmed and crossDown
plotshape(confirmedUp, "Confirmed cross above zero", shape.triangleup, location.bottom, color.green, size=size.tiny)
plotshape(confirmedDown, "Confirmed cross below zero", shape.triangledown, location.top, color.red, size=size.tiny)
alertcondition(confirmedUp, "DPO above zero", "Non-centered DPO crossed above zero on a closed bar.")
alertcondition(confirmedDown, "DPO below zero", "Non-centered DPO crossed below zero on a closed bar.")The alert conditions and triangles use only the blue series, require a confirmed bar, and have no negative offset. Select the desired condition in TradingView's Create Alert dialog and use Once Per Bar Close. The script contains no orders or performance calculation.
The blue line may change during an open bar because close changes. The orange plot waits for a confirmed calculation bar before adding a point to the past; confirmation does not make it available earlier. Read TradingView's repainting and historical plotting guidance when evaluating chart signals.
Checks before interpreting a cycle
- Keep missing history as unavailable. With consecutive valid closes, the non-centered series first exists after N + s bars; the research calculation needs max(N, s + 1). For N = 21, those counts are 32 and 21. Do not replace warm-up values with zero and create artificial crossings.
- Use the same symbol, timeframe, session and source when comparing scripts. A length of 21 means 21 chart bars, not a promise of a monthly cycle. Choose the length for a stated research question and test its sensitivity.
- Read amplitude in the instrument's price units. A DPO value of 5 has no universal overbought meaning and cannot be compared directly across instruments with different price scales.
- Do not assume detrending removes every directional component. For a linearly rising close C[t] = a + bt, the non-centered formula equals b × (s + (N−1)/2). With N = 4 and b = 1, it stays at +4.5 after warm-up even though the series has no oscillating cycle.
- Separate cycle inspection from direction and trade rules. A zero crossing alone provides no stop, exit, position size or cost model. ROC answers a different question: price change relative to a prior price. CMO compares upward and downward changes.
This page is an indicator calculation guide, not investment advice or evidence of a profitable strategy.
Specify the timing contract in Pineify
Use the Pine Script AI Coding Agent to adapt inputs and alert rules, then inspect the generated source. A useful request names both formulas and forbids moving signal markers into the past:
Create a DPO indicator with length 21 and an explicit floor(N / 2) + 1 shift. Keep close minus the older SMA at offset zero. Make historical close minus current SMA an optional research plot with a negative offset. Generate alerts only from confirmed non-centered values. Expose both SMA inputs in the Data Window.
First compare individual bars with the formulas above. Then define entries, exits, fees and slippage separately if you turn the indicator into a strategy. A syntax check establishes compilation, not trading validity.
Open Pine Script AI Coding AgentDPO questions
Sources and related indicators
Official documentation checked October 17, 2026. The worked prices and arithmetic are illustrative.
- TradingView: DPO definition, formula and Centered setting
- Pine Script documentation: plot offsets
- Pine Script documentation: repainting and plotting in the past
- Pine Script v6 migration: integer division
Browse the TradingView indicator guides to compare calculation and signal-timing rules.