PMARP: Ratio Ranking, Ties and Pine Script | Pineify
PMARP ranks the current price-to-moving-average ratio against valid ratios from earlier bars. The rank gives relative historical ordering, not a distance from the average or a reversal probability. This implementation includes ties, excludes the current observation from the comparison sample, and uses partial history during startup.
Inspect the Pine ScriptSeparate ratio, deviation and rank
PMAR = source / MA. For a positive MA, PMAR 1.05 means source is 5% above the average, and 0.95 means it is 5% below. Percentage deviation is 100 × (PMAR − 1), while absolute distance from the ratio baseline would be abs(PMAR − 1).
This code ranks abs(PMAR), not abs(PMAR − 1). With positive source and positive MA, abs(PMAR) is simply PMAR: 0.95 ranks below 1.05 even though both are 5% from the average. Do not describe it as a direction-free distance rank. Negative or zero-crossing sources need a separate interpretation because the absolute-value transform changes ordering.
For prior offsets 1 through lookback, the loop counts valid historical absolute ratios. Rank is 100 × count(previous absolute ratio ≤ current absolute ratio) / valid sample count. The current bar is excluded from that denominator. A rank of 95 describes this sample comparison, not a 95% probability of a pullback.
The_Caretaker published Price Moving Average Ratio & Percentile in July 2021. The local copy has its own defaults and display choices; this guide does not claim Pineify invented the concept in 2025 or that the copies are identical.
Count ties explicitly: a flat ratio can rank at 100
Assume the age gate is satisfied and the prior valid ratios are 0.8, 0.9, 1.0, 1.0 and 1.1. They are all positive, so the absolute-value operation does not change them. These are hypothetical ratio inputs to inspect the ranking stage, not a backtest.
| Current PMAR | Count ≤ current | Rank | Deviation from positive MA |
|---|---|---|---|
| 0.95 | 2 | 40 | −5% |
| 1 | 4 | 80 | 0% |
| 1.05 | 4 | 80 | +5% |
| 1.2 | 5 | 100 | +20% |
If current and every historical ratio equal 1, all comparisons pass and the result is 100, even though price equals its MA. The formula uses the inclusive empirical rank, not a midrank or strict-less-than convention. The green 50 reference therefore does not guarantee a tradable neutral state or even a value the discrete sample can produce.
Against prior ratios 0.8, 0.85, 0.9 and 0.92, a current ratio of 0.95 ranks at 100 while price is below its MA. Against 1.1, 1.2 and 1.3, a current ratio of 1.05 ranks at 0 while price is above its MA. Rank alone cannot establish that direction.
Lookback changes the sample, not a universal strictness setting
The loop examines up to 350 prior chart bars by default and skips missing historical ratios. It can use fewer than 350 valid samples. The additional bar-index condition is only an age gate; it does not require 350 complete observations. Under ordinary complete VWMA-20 history, the first valid ratio is at zero-based bar 19 and the next bar can rank against just that one prior ratio.
A one-observation sample produces 0 or 100. With 350 valid observations, the grid spacing is 100/350, about 0.285714. The strict background rule above 99 requires at least 347 counted observations; the rule below 1 permits counts 0 through 3. An equal reading of exactly 99 or 1 does not satisfy its respective strict rule.
Increasing lookback can raise or lower the rank. For the five-observation example and current ratio 0.95, rank is 40. Add an older ratio 0.5 and it becomes 3/6 = 50; add 1.5 instead and it becomes 2/6 ≈ 33.3333. There is no fixed percentage by which a shorter window becomes looser.
350 fifteen-minute bar intervals total 5,250 minutes, or 87.5 hours. That is about 3.65 days only for a continuously trading market; sessions, holidays and missing bars change elapsed calendar time. Do not interpret the same bar count as a fixed number of calendar months across instruments.
Invalid current ratios must remain missing
The ratio is missing when its MA denominator is zero or missing, or when the source is missing. The local helper previously checked historical sample count without separately requiring a valid current ratio. Every comparison against a missing current value then failed, which could produce an artificial rank of zero despite no valid current observation.
This page’s copy adds a current-ratio validity condition before returning the rank. That is a deliberate difference from the inspected local helper, not a claim that every product-generated version already includes the correction. Missing current ratios now produce no main value rather than a false low-percentile reading.
The default VWMA uses reported volume. This example has no independent feed-quality warning or explicit consecutive-valid-volume policy. Inspect the selected feed and intermediate MA rather than treating a missing-data repair as evidence of trading activity. The signal SMA can retain information from earlier valid rank values even when the current main value is missing.
The ZLEMA helper uses an explicit integer history index, preserving this local router’s floor(length/2) lag policy. Other helper branches retain their own initialization, including zero-seeded VAR and WWMA. For their meaning and limits, see the moving-average router reference; the default calculation here is VWMA.
Read the display that the supplied code actually draws
The final call fixes close, percentile mode, VWMA length 20, lookback 350, and SMA signal length 20. It has no settings inputs. The main line maps rank 0 to blue, 50 to green and 100 to red. The white signal line averages the rank. All scale and threshold references are ordinary solid plots, not a dashed midline.
Background coloring is a state condition above 99 or below 1, so it can repeat on consecutive bars. It is not a newly detected crossover, and there are no alertcondition calls. The words “Alert Level” in plot titles do not create notifications. The code also creates no labels, despite its label-count allowance.
The helper separately tracks raw PMAR high and low over available dataset history, seeded around 1; those are not rolling 350-bar extrema. Their color score is not used for the current percentile-colored plot. Changing only the helper’s usePmarp argument would leave the hard-coded percentile scales, thresholds and color selection inconsistent. A raw-ratio adaptation must change the display logic as well.
Current close and VWMA can change on the live bar. No confirmation gate is implemented. A real alert or strategy must specify its event, equality policy, and confirmation timestamp. See Pine live-bar behavior.
Choose a comparison by its formula
Kairi reports a percentage deviation from an average. On matching positive inputs it equals 100 × (PMAR − 1). RSI instead compares smoothed up and down changes. Neither becomes a performance benchmark merely because it uses familiar thresholds.
Bollinger %B measures (price − lower band)/(upper band − lower band). It can exceed 1 or fall below 0 when price is outside the bands, as TradingView documents; it is not confined to 0–1. PMARP is bounded by its count fraction when valid, but that does not give it a probability interpretation.
Before any strategy study, retain both the raw ratio and sample count, define the event and exits, include costs and evaluate a separate period. A narrow historical distribution can give large rank changes for tiny ratio changes. Trend and volatility filters remain testable hypotheses, not established improvements in win rate.
Pine Script v6: implementation and limits
The complete copy adds a validity guard for the current ratio and makes the ZLEMA history index an integer. It preserves the inclusive ranking and partial-history policy. Verify ratio, sample count, ties and missing cases before adding a signal; syntax success alone does not validate a trading rule.
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © Pineify
//@version=6
indicator(title="Price Moving Average Ratio & Percentile", overlay=false, max_labels_count=500)
p_comm_time_range_to_unix_time(string time_range, int date_time = time, string timezone = syminfo.timezone) =>
int start_unix_time = na
int end_unix_time = na
int start_time_hour = na
int start_time_minute = na
int end_time_hour = na
int end_time_minute = na
if str.length(time_range) == 11
start_time_hour := math.floor(str.tonumber(str.substring(time_range, 0, 2)))
start_time_minute := math.floor(str.tonumber(str.substring(time_range, 3, 5)))
end_time_hour := math.floor(str.tonumber(str.substring(time_range, 6, 8)))
end_time_minute := math.floor(str.tonumber(str.substring(time_range, 9, 11)))
else if str.length(time_range) == 9
start_time_hour := math.floor(str.tonumber(str.substring(time_range, 0, 2)))
start_time_minute := math.floor(str.tonumber(str.substring(time_range, 2, 4)))
end_time_hour := math.floor(str.tonumber(str.substring(time_range, 5, 7)))
end_time_minute := math.floor(str.tonumber(str.substring(time_range, 7, 9)))
start_unix_time := timestamp(timezone, year(date_time, timezone), month(date_time, timezone), dayofmonth(date_time, timezone), start_time_hour, start_time_minute, 0)
end_unix_time := timestamp(timezone, year(date_time, timezone), month(date_time, timezone), dayofmonth(date_time, timezone), end_time_hour, end_time_minute, 0)
[start_unix_time, end_unix_time]
p_custom_sources() =>
[open, high, low, close, volume]
p_ta_Var_Func(src, length) =>
valpha = 2 / (length + 1)
vud1 = src > src[1] ? src - src[1] : 0
vdd1 = src < src[1] ? src[1] - src : 0
vUD = math.sum(vud1, 9)
vDD = math.sum(vdd1, 9)
vCMO = nz((vUD - vDD) / (vUD + vDD))
VAR = 0.0
VAR := nz(valpha * math.abs(vCMO) * src) + (1 - valpha * math.abs(vCMO)) * nz(VAR[1])
VAR
p_ta_Wwma_Func(src, length) =>
wwalpha = 1 / length
WWMA = 0.0
WWMA := wwalpha * src + (1 - wwalpha) * nz(WWMA[1])
WWMA
p_ta_Zlema_Func(src, length) =>
zxLag = int(math.floor(length / 2.0))
zxEMAData = src + src - src[zxLag]
ZLEMA = ta.ema(zxEMAData, length)
ZLEMA
p_ta_Tsf_Func(src, length) =>
lrc = ta.linreg(src, length, 0)
lrc1 = ta.linreg(src, length, 1)
lrs = lrc - lrc1
TSF = ta.linreg(src, length, 0) + lrs
TSF
p_ta_ma(series float source, simple int length, simple string ma_type) =>
switch ma_type
"SMA" => ta.sma(source, length)
"EMA" => ta.ema(source, length)
"RMA" => ta.rma(source, length)
"HMA" => ta.hma(source, length)
"WMA" => ta.wma(source, length)
"TMA" => ta.sma(ta.sma(source, math.ceil(length / 2)), math.floor(length / 2) + 1)
"VAR" => p_ta_Var_Func(source, length)
"WWMA" => p_ta_Wwma_Func(source, length)
"ZLEMA" => p_ta_Zlema_Func(source, length)
"TSF" => p_ta_Tsf_Func(source, length)
"VWMA" => ta.vwma(source, length)
p_ta_pmarp(series float source, simple bool usePmarp, simple int pmarLength, simple string maType, simple int lookback, simple int signalLength, simple string signalMaType) =>
ma = p_ta_ma(source, pmarLength, maType)
pmar = ma != 0 ? source / ma : na
pmarRankValue = math.abs(pmar)
rankCount = 0
sampleCount = 0
for i = 1 to lookback
if not na(pmarRankValue[i])
sampleCount += 1
if pmarRankValue[i] <= pmarRankValue
rankCount += 1
pmarp = not na(pmarRankValue) and bar_index >= pmarLength and sampleCount > 0 ? rankCount * 100.0 / sampleCount : na
pmarHigh = math.max(1.0, nz(pmar, 1.0))
pmarHigh := math.max(nz(pmarHigh[1], 1.0), nz(pmar, 1.0))
pmarLow = math.min(1.0, nz(pmar, 1.0))
pmarLow := math.min(nz(pmarLow[1], 1.0), nz(pmar, 1.0))
highRange = pmarHigh - 1.0
lowRange = 1.0 - pmarLow
pmarColorPercent = pmar >= 1.0 ? (highRange > 0 ? ((pmar - 1.0) * 100.0 / highRange) / 2.0 + 50.0 : 50.0) : (lowRange > 0 ? ((pmar - pmarLow) * 100.0 / lowRange) / 2.0 : 50.0)
pmarColorPercent := math.min(100.0, math.max(0.0, pmarColorPercent))
plotLine = usePmarp ? pmarp : pmar
signal = p_ta_ma(plotLine, signalLength, signalMaType)
[plotLine, signal, pmarHigh, pmarLow, pmarColorPercent]
[p_ind_1_line, p_ind_1_signal, p_ind_1_pmarHigh, p_ind_1_pmarLow, p_ind_1_colorPercent] = p_ta_pmarp(close, true, 20, "VWMA", 350, 20, "SMA")
p_ind_1_line_spectrumValue = true ? p_ind_1_line : p_ind_1_colorPercent
p_ind_1_line_color = p_ind_1_line_spectrumValue <= 50 ? color.from_gradient(p_ind_1_line_spectrumValue, 0, 50, color.rgb(0, 0, 255, 0), color.rgb(0, 255, 0, 0)) : color.from_gradient(p_ind_1_line_spectrumValue, 50, 100, color.rgb(0, 255, 0, 0), color.rgb(255, 0, 0, 0))
p_ind_1_signal_color = color.rgb(255, 255, 255, 0)
p_ind_1_line_highAlert = true ? 99 : 1
p_ind_1_line_lowAlert = true ? 1 : 1
p_ind_1_line_highBgColor = true ? color.new(p_ind_1_line_color, 50) : color.new(color.rgb(255, 0, 0, 0), 50)
p_ind_1_line_lowBgColor = true ? color.new(p_ind_1_line_color, 50) : color.new(color.rgb(0, 0, 255, 0), 50)
plot(true ? 100 : na, title="PMARP - Scale High", color=color.rgb(255, 0, 0, 0), linewidth=1)
plot(true ? 50 : 1, title="PMARP - Midline", color=color.rgb(255, 255, 255, 40), linewidth=1)
plot(true ? 0 : na, title="PMARP - Scale Low", color=color.rgb(0, 0, 255, 0), linewidth=1)
plot(p_ind_1_line, title="PMARP - Plot Line", color=p_ind_1_line_color, linewidth=1)
plot(true ? na : p_ind_1_pmarHigh, title="PMARP - Historical PMAR High", color=color.rgb(255, 0, 0, 0), linewidth=1)
plot(true ? na : p_ind_1_pmarLow, title="PMARP - Historical PMAR Low", color=color.rgb(0, 0, 255, 0), linewidth=1)
plot(p_ind_1_signal, title="PMARP - Signal Line", color=p_ind_1_signal_color, linewidth=1, display=display.all)
plot(p_ind_1_line_highAlert, title="PMARP - High Alert Level", color=color.rgb(255, 0, 0, 0), linewidth=1)
plot(p_ind_1_line_lowAlert, title="PMARP - Low Alert Level", color=color.rgb(0, 0, 255, 0), linewidth=1)
bgcolor(p_ind_1_line > p_ind_1_line_highAlert ? p_ind_1_line_highBgColor : na, title="PMARP - High Signal Bars")
bgcolor(p_ind_1_line < p_ind_1_line_lowAlert ? p_ind_1_line_lowBgColor : na, title="PMARP - Low Signal Bars")
Specify a checkable adaptation in Pineify
Ask the Pine Script AI Coding Agent to specify inclusive versus strict ranking, the minimum valid sample count, current-data validity, MA type and seed, and confirmed event timing. Request raw PMAR and sample count in the Data Window, and test the tie and below-MA/high-rank examples. If adding a raw mode, keep its plot scale and threshold units consistent.
Open Pine Script AI Coding AgentEducational indicator analysis. No measured trading performance is established here. Syntax validation does not prove formula equivalence, execution assumptions or profitability.