TradingView indicator comparisons

PPO vs MACD: percentage normalization across symbols

MACD measures the fast EMA minus the slow EMA in price units. PPO divides that difference by the slow EMA and multiplies by 100. Multiplying an entire positive price history by 10 multiplies MACD by 10 but leaves PPO unchanged, provided initialization and validity rules scale with it. PPO therefore removes price-level scale from the comparison. It does not normalize volatility, establish a universal trading threshold or guarantee the same signal-line crossing as MACD.

See the decision guide

Keep each oscillator, signal and histogram in its own units

Let F and L be fast and slow EMAs of the same source. This page uses EMA smoothing for both the oscillator and its signal. TradingView also exposes moving-average type settings, so match those settings before comparing a built-in indicator with the script.

  • PPO = 2 means the fast EMA is 2% above the slow EMA. It is not a 2% holding-period return, forecast or risk estimate.
  • Subtracting a MACD signal from PPO mixes price units with percent. Dividing the MACD histogram by the current slow EMA also does not generally produce the PPO histogram.
  • With a changing denominator, EMA(100 × M/L) generally differs from 100 × EMA(M)/L. The numerical counterexample below makes that difference visible.
SeriesMACDPPO
Main lineM = F − L, in price unitsP = 100 × (F − L) / L, in percent
SignalS_M = EMA(M, signal length), price unitsS_P = EMA(P, signal length), percent
HistogramM − S_M, price unitsP − S_P, percentage points
Positive main lineF > LF > L, provided L > 0

Same prices multiplied by 10: one controlled experiment

Use the synthetic sequence 100, 110, 130, 160, 200, 240, 260, 270, 275, 280, 285, 290 on bars 0 through 11. Create a second series by multiplying every observation by 10. Set fast length 3, slow length 5 and signal length 3. Seed each price EMA with its first observation; seed each signal with its first valid oscillator value, zero here. These short lengths make the arithmetic inspectable and are not recommended trading settings.

  • EMA_N(t) = a × x(t) + (1 − a) × EMA_N(t−1), where a = 2/(N+1). At bar 1, F = 0.5 × 110 + 0.5 × 100 = 105 and L = (1/3) × 110 + (2/3) × 100 = 103.333333. Thus M = 1.666667 and P = 100 × (5/3)/(310/3) = 1.612903%.
  • For a constant multiplier c > 0 and scaled seeds, linearity gives F(cx) = cF(x) and L(cx) = cL(x). Hence M(cx) = cM(x), while P(cx) = 100 × cM(x)/(cL(x)) = P(x). This is a formula proof; the table is a separate reproducible check.
  • MACD signal and histogram also multiply by c. PPO signal and histogram stay unchanged if they see the same valid observations and use identical seeds. Keep full precision internally; the table rounds only for display.
  • This proof scales the whole history. A split adjustment applied to only part of the history, an additive offset, a changing currency conversion or different missing bars is a different transformation. Check feed adjustments and timestamps before applying the conclusion.
BarBase priceBase slow EMABase MACD10x MACDPPO for either series
0100100.0000000.0000000.0000000.000000%
1110103.3333331.66666716.6666671.612903%
4200152.09876517.276235172.76234611.358563%
7270228.39963422.772241227.7224089.970349%
8275243.93308919.152848191.5284807.851681%

Zero crossings match under positive denominators; signal crosses can differ

For L > 0, multiplying M by 100/L preserves its sign and its zeros. Consequently, main-line crossings of zero match on consecutive valid bars with identical inputs and confirmation times. This statement does not extend to a crossing of each oscillator against its own smoothed signal. In the same fixture, the rising slow EMA compresses recent PPO readings relative to earlier ones.

  • Define a downward signal cross as histogram < 0 now and histogram ≥ 0 on the preceding bar. PPO crosses down on bar 7; MACD crosses down on bar 8. Both main lines remain positive throughout these rows.
  • The counterexample uses identical price observations, EMA lengths and completed-bar timestamps. The discrepancy comes from normalizing before signal smoothing. It does not establish that PPO always leads.
  • Multiplying this fixture by 10 preserves the crossing timestamps for each oscillator. Scale invariance within PPO is different from equivalence between PPO and MACD.
BarMACDMACD signalMACD histogramPPOPPO signalPPO histogram
624.74429921.207347+3.53695111.919251%11.278226%+0.641024 pp
722.77224121.989794+0.7824479.970349%10.624288%−0.653939 pp
819.15284820.571321−1.4184737.851681%9.237984%−1.386303 pp

Reject a denominator that cannot support the comparison

The script accepts PPO only when the slow EMA exceeds a positive minimum expressed in source price units. The default floor is 0.000001 for the base series and ten times that for the 10x series. This floor is an explicit example policy, not a universal market constant. Select it for the instrument and data precision before evaluating thresholds.

  • A numerator of 0.01 over a denominator of 0.000001 gives 1,000,000%. That finite answer does not make it a useful cross-symbol momentum comparison. Using abs(L) or adding epsilon silently changes the definition and is not a repair.
  • Scaling prices also requires scaling an absolute denominator floor. Otherwise one copy can be rejected while the other passes, even though the underlying formula is scale invariant.
  • With a negative denominator, the formal ratio reverses sign. Synthetic spreads, transformed data and histories containing nonpositive prices need an explicitly different specification; this page refuses their PPO interpretation.
  • After a missing or rejected PPO observation, the next valid PPO starts a new signal EMA at that PPO value. Its first histogram is zero. No crossing is inferred across the gap. This reset policy is deliberate and can differ from an indicator that carries its old signal state.
Slow EMA conditionWhat happensPolicy in this example
Exactly zeroDivision is undefinedPPO, signal and histogram are unavailable
NegativeA positive MACD can produce a negative PPOReject PPO; do not interpret its sign as the fast/slow ordering
Positive but at or below the floorSmall denominator can magnify small absolute errorsReject rather than replace the denominator with the floor
Source is missingNo new price observation existsHold price EMA and MACD signal states; show a gap and reset PPO signal

Run the matched Pine Script v6 audit

Paste the script into Pine Editor. The default Fixture mode uses the first loaded chart bar as bar 0, holds 290 after bar 11, and needs at least 12 completed bars for the full table. Use the Data Window to locate bars and read numerical residuals. The pane shows either percentage units or price units, controlled by the input switch, so the two unit systems never share its plotted scale.

  • With defaults and no gap, the 10x PPO markers overlap the base PPO. Switch off percent units to see the MACD magnitudes differ by 10. Four Data Window residuals should be zero within floating-point tolerance. P marks bar 7 and M marks bar 8.
  • Zero, Negative and Near zero modes exercise rejection. A red background and PPO valid = 0 disclose unavailable PPO; MACD can remain defined. Near zero multiplies the fixture by 1e−10, so all slow EMAs remain below the default floor.
  • Enable the gap switch to omit bar 4. The EMA states pause on that bar; neither oscillator displays a value. At bar 5, the slow EMA is 165.432099, MACD is 23.942901 and PPO is 14.472948%. The reset PPO signal equals PPO there. These values differ from the uninterrupted fixture.
  • Chart close mode uses the same chart closes for both versions. The first loaded close seeds both price EMAs; changing loaded history can change early outputs. Match source, exchange, session, adjustment, timeframe, lengths and history before comparing another implementation.
  • All states update from available observations; barstate.isconfirmed gates the plots and crossing markers. This script has no higher-timeframe request or negative plot offset. A syntax check establishes supported compilation syntax, while numerical and timestamp checks establish these specific behaviors.
Pine Script v6 indicator with explicit seeds, denominator rejection and completed-bar output. It contains no orders or backtest.pine
//@version=6
indicator("PPO vs MACD: scale audit", overlay = false, precision = 6)
mode = input.string("Fixture", "Source", options = ["Fixture", "Chart close", "Zero", "Negative", "Near zero"])
showPpo = input.bool(true, "Plot percent units (off = price units)")
insertGap = input.bool(false, "Fixture: omit bar 4")
fastLength = input.int(3, "Fast EMA", minval = 1, maxval = 500)
slowLength = input.int(5, "Slow EMA", minval = 2, maxval = 500)
signalLength = input.int(3, "Signal EMA", minval = 1, maxval = 500)
floorValue = input.float(0.000001, "Minimum slow EMA (base price units)", minval = 0.000000000001)
if fastLength >= slowLength
    runtime.error("Fast EMA must be shorter than slow EMA")
float fixture = switch bar_index
    0 => 100.0
    1 => 110.0
    2 => 130.0
    3 => 160.0
    4 => 200.0
    5 => 240.0
    6 => 260.0
    7 => 270.0
    8 => 275.0
    9 => 280.0
    10 => 285.0
    => 290.0
float src = mode == "Chart close" ? close : mode == "Zero" ? 0.0 : mode == "Negative" ? -fixture : mode == "Near zero" ? fixture * 1e-10 : fixture
if insertGap and mode == "Fixture" and bar_index == 4
    src := na
metrics(float x, float minSlow) =>
    var float fast = na
    var float slow = na
    var float macdSignal = na
    var float ppoSignal = na
    float af = 2.0 / (fastLength + 1)
    float al = 2.0 / (slowLength + 1)
    float ag = 2.0 / (signalLength + 1)
    if not na(x)
        fast := na(fast) ? x : af * x + (1.0 - af) * fast
        slow := na(slow) ? x : al * x + (1.0 - al) * slow
    float macd = na(x) ? na : fast - slow
    if not na(macd)
        macdSignal := na(macdSignal) ? macd : ag * macd + (1.0 - ag) * macdSignal
    bool valid = not na(x) and not na(slow) and slow > minSlow
    float ppo = valid ? 100.0 * macd / slow : na
    if valid
        ppoSignal := na(ppoSignal) ? ppo : ag * ppo + (1.0 - ag) * ppoSignal
    else
        ppoSignal := na
    [macd, na(x) ? na : macdSignal, ppo, ppoSignal, slow, valid]
[m, ms, p, ps, slow, valid] = metrics(src, floorValue)
[m10, ms10, p10, ps10, slow10, valid10] = metrics(src * 10.0, floorValue * 10.0)
mh = m - ms
ph = p - ps
bool confirmed = barstate.isconfirmed
bool macdDown = mh < 0 and mh[1] >= 0
bool ppoDown = ph < 0 and ph[1] >= 0
plot(confirmed ? (showPpo ? p : m) : na, "Base oscillator", color.blue, 2, plot.style_linebr)
plot(confirmed ? (showPpo ? ps : ms) : na, "Base signal", color.orange, 1, plot.style_linebr)
plot(confirmed ? (showPpo ? ph : mh) : na, "Base histogram", color.gray, style = plot.style_columns)
plot(confirmed ? (showPpo ? p10 : m10) : na, "10x oscillator, same selected units", color.purple, style = plot.style_circles)
hline(0, "Zero")
plotshape(confirmed and macdDown, "MACD cross below signal", shape.triangledown, location.top, color.blue, text = "M")
plotshape(confirmed and ppoDown, "PPO cross below signal", shape.triangledown, location.bottom, color.orange, text = "P")
plot(bar_index, "Fixture bar", display = display.data_window)
plot(confirmed ? slow : na, "Base slow EMA, price units", display = display.data_window)
plot(confirmed ? slow10 : na, "10x slow EMA, price units", display = display.data_window)
plot(confirmed ? m : na, "MACD, price units", display = display.data_window)
plot(confirmed ? ms : na, "MACD signal, price units", display = display.data_window)
plot(confirmed ? p : na, "PPO, percent", display = display.data_window)
plot(confirmed ? ps : na, "PPO signal, percent", display = display.data_window)
plot(confirmed ? m10 - 10.0 * m : na, "MACD scale residual", display = display.data_window)
plot(confirmed ? ms10 - 10.0 * ms : na, "MACD signal scale residual", display = display.data_window)
plot(confirmed ? p10 - p : na, "PPO scale residual", display = display.data_window)
plot(confirmed ? ps10 - ps : na, "PPO signal scale residual", display = display.data_window)
plot(confirmed ? (valid and valid10 ? 1 : 0) : na, "PPO valid: 1=yes, 0=rejected", display = display.data_window)
bgcolor(confirmed and not valid ? color.new(color.red, 85) : na, title = "PPO rejected")

Use percentage units for comparison without assuming equal risk

PPO is useful when the question is the EMA gap relative to the slow price average across differently priced instruments. A fixed absolute MACD threshold answers a price-unit question and must change when the entire price series is rescaled. Neither normalization gives a universal buy or sell threshold.

  • Two instruments can both show PPO = 2% while having different return volatility, liquidity, tick size, trading sessions and gap risk. Dividing by price does not divide by volatility. Compare the distribution of readings and subsequent outcomes within each instrument and timeframe.
  • Keep the same observation frequency and EMA settings when forming a cross-symbol screen. A 12-bar EMA on five-minute bars and a 12-bar EMA on daily bars measure different horizons.
  • Define threshold units and invalid-data exclusions before testing. If the condition is PPO > 2, it means 2%, not 0.02%. A PPO histogram threshold is in percentage points. Evaluate costs, fills and untouched periods separately if you turn the condition into a strategy.
  • For a custom implementation, give the Pine Script AI Coding Agent the source, EMA seeds, denominator floor, gap policy and confirmation rule. Ask it to draft the comparison and explicitly request a syntax check. Reconcile values and event timestamps before changing trading rules.
Where Pineify fits

Pine Script AI Coding Agent

Draft a comparison with explicit units and invalid-data rules. Request a syntax check, then inspect values and crossing timestamps against the fixture.

Draft my oscillator comparison

Frequently asked questions

Educational comparison, not investment advice. Synthetic prices demonstrate formulas and do not represent market returns. Syntax validation does not establish profitable signals or correct trade execution.

Sources and verification

Published by Pineify. Pineify is an independent product and is not affiliated with TradingView. Sources support the technical explanations; examples do not establish trading performance.