Percentile Nearest Rank (PNR)

Parameters: length = 15 | percentage = 50 (0–100)

Overview

The Percentile Nearest Rank indicator identifies the value at any specified percentile within a rolling window of recent prices, providing a dynamic measure of where price stands relative to its recent distribution. PNR sorts the values within each lookback window and returns the element that corresponds to the chosen percentile, effectively creating a moving percentile line that adapts to changing market conditions. Setting the percentage to 50 produces a rolling median that's more robust to outliers than a simple average, while percentiles like 25 or 75 create dynamic support and resistance levels based on recent price distribution. Traders use extreme percentiles (95th or 5th) to identify overbought and oversold conditions, as price breaking above the 95th percentile suggests exceptional strength while falling below the 5th percentile indicates unusual weakness. The indicator proves particularly valuable for adaptive trading systems that need to adjust thresholds based on recent market behavior rather than fixed levels.

Implementation Examples

Compute PNR from a slice or candles:

use vectorta::indicators::percentile_nearest_rank::{
    percentile_nearest_rank, PercentileNearestRankInput, PercentileNearestRankParams
};
use vectorta::utilities::data_loader::{Candles, read_candles_from_csv};

// Using a price slice
let prices = vec![100.0, 101.0, 102.5, 101.5, 103.0];
let params = PercentileNearestRankParams { length: Some(15), percentage: Some(50.0) }; // defaults
let input = PercentileNearestRankInput::from_slice(&prices, params);
let result = percentile_nearest_rank(&input)?;

// Using Candles with defaults (length=15, percentage=50, source="close")
let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let input = PercentileNearestRankInput::with_default_candles(&candles);
let result = percentile_nearest_rank(&input)?;

// Access values
for v in result.values { println!("PNR: {}", v); }

API Reference

Input Methods
// From price slice
PercentileNearestRankInput::from_slice(&[f64], PercentileNearestRankParams) -> PercentileNearestRankInput

// From candles with custom source
PercentileNearestRankInput::from_candles(&Candles, &str, PercentileNearestRankParams) -> PercentileNearestRankInput

// From candles with defaults (length=15, percentage=50, source="close")
PercentileNearestRankInput::with_default_candles(&Candles) -> PercentileNearestRankInput
Parameters Structure
#[derive(Debug, Clone)]
pub struct PercentileNearestRankParams {
    pub length: Option<usize>,     // Default: 15
    pub percentage: Option<f64>,   // Default: 50.0
}
Output Structure
pub struct PercentileNearestRankOutput {
    pub values: Vec<f64>, // Rolling nearest-rank percentile values (NaN during warmup)
}
Validation, Warmup & NaNs
  • length > 0 and length ≤ len(data); else PercentileNearestRankError::InvalidPeriod.
  • percentage ∈ [0, 100] and finite; else PercentileNearestRankError::InvalidPercentage.
  • Empty input: PercentileNearestRankError::EmptyInputData. All values NaN: PercentileNearestRankError::AllValuesNaN.
  • After the first finite input at first, require len(data) − first ≥ length or NotEnoughValidData.
  • Warmup: indices ..(first + length − 1) are NaN in batch; streaming returns None until filled.
  • Per-step window ignores NaNs; if a window is entirely NaN, output is NaN for that step.
Error Handling
use vectorta::indicators::percentile_nearest_rank::{
    percentile_nearest_rank, PercentileNearestRankError
};

match percentile_nearest_rank(&input) {
    Ok(output) => process(output.values),
    Err(PercentileNearestRankError::EmptyInputData) => println!("Input data is empty"),
    Err(PercentileNearestRankError::AllValuesNaN) => println!("All input values are NaN"),
    Err(PercentileNearestRankError::InvalidPeriod { period, data_len }) =>
        println!("Invalid length {} for data length {}", period, data_len),
    Err(PercentileNearestRankError::InvalidPercentage { percentage }) =>
        println!("Invalid percentage {} (must be 0..=100)", percentage),
    Err(PercentileNearestRankError::NotEnoughValidData { needed, valid }) =>
        println!("Need {} valid points after first finite, only {}", needed, valid),
}

Python Bindings

Basic Usage

Calculate PNR using NumPy arrays (defaults: length=15, percentage=50):

import numpy as np
from vectorta import percentile_nearest_rank

prices = np.array([100.0, 101.0, 102.5, 101.5, 103.0])

# With explicit parameters
result = percentile_nearest_rank(prices, length=15, percentage=50.0)

# Specify kernel for optimization ("auto" | "avx2" | "avx512" if available)
result = percentile_nearest_rank(prices, length=15, percentage=90.0, kernel="auto")

print(result)  # NumPy array
Streaming Real-time Updates

Update an internal state and receive values after warmup:

from vectorta import PercentileNearestRankStream

stream = PercentileNearestRankStream(length=15, percentage=50.0)
for price in price_feed:
    val = stream.update(price)
    if val is not None:
        handle(val)
Batch Parameter Optimization

Test multiple length and percentile values efficiently:

import numpy as np
from vectorta import percentile_nearest_rank_batch

prices = np.array([...])
results = percentile_nearest_rank_batch(
    prices,
    length_range=(10, 50, 10),
    percentage_range=(25.0, 75.0, 25.0),
    kernel=None
)

print(results['values'].shape)  # (num_combinations, len(prices))
print(results['lengths'])        # list of tested lengths
print(results['percentages'])    # list of tested percentiles
CUDA Acceleration

CUDA support for PNR is currently under development. The API will follow the same pattern as other CUDA-enabled indicators.

# Coming soon: CUDA-accelerated PNR calculations
# from vectorta import percentile_nearest_rank_cuda_batch, percentile_nearest_rank_cuda_many_series_one_param
# ...

JavaScript/WASM Bindings

Basic Usage

Compute PNR in JavaScript/TypeScript:

import { percentile_nearest_rank_js } from 'vectorta-wasm';

const prices = new Float64Array([100.0, 101.0, 102.5, 101.5, 103.0]);
const values = percentile_nearest_rank_js(prices, 15, 50.0);
console.log('PNR values:', Array.from(values));
Memory-Efficient Operations

Use zero-copy APIs for performance:

import { percentile_nearest_rank_alloc, percentile_nearest_rank_free, percentile_nearest_rank_into, memory } from 'vectorta-wasm';

const prices = new Float64Array([/* data */]);
const n = prices.length;
const inPtr = percentile_nearest_rank_alloc(n);
const outPtr = percentile_nearest_rank_alloc(n);

try {
  new Float64Array(memory.buffer, inPtr, n).set(prices);
  // Args: data_ptr, out_ptr, len, length, percentage
  percentile_nearest_rank_into(inPtr, outPtr, n, 15, 50.0);
  const out = new Float64Array(memory.buffer, outPtr, n).slice();
  console.log(out);
} finally {
  percentile_nearest_rank_free(inPtr, n);
  percentile_nearest_rank_free(outPtr, n);
}
Batch Processing

Run a unified batch over length and percentile values and get metadata:

import { percentile_nearest_rank_batch } from 'vectorta-wasm';

const prices = new Float64Array([/* historical prices */]);
const { values, combos, rows, cols } = percentile_nearest_rank_batch(prices, {
  length_range: [10, 50, 10],
  percentage_range: [25.0, 75.0, 25.0]
});

// Reshape values to a matrix [rows x cols]
const matrix: number[][] = [];
for (let r = 0; r < rows; r++) {
  const start = r * cols;
  matrix.push(Array.from(values.slice(start, start + cols)));
}

// Access parameters for the first combo
const firstLen = (combos[0] as any).length;
const firstPct = (combos[0] as any).percentage;

Performance Analysis

Comparison:
View:
Loading chart...

AMD Ryzen 9 9950X (CPU) | NVIDIA RTX 4090 (GPU) | Benchmarks: 2026-01-05

Related Indicators