Percentile Nearest Rank (PNR)
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 > 0andlength ≤ len(data); elsePercentileNearestRankError::InvalidPeriod.percentage ∈ [0, 100]and finite; elsePercentileNearestRankError::InvalidPercentage.- Empty input:
PercentileNearestRankError::EmptyInputData. All valuesNaN:PercentileNearestRankError::AllValuesNaN. - After the first finite input at
first, requirelen(data) − first ≥ lengthorNotEnoughValidData. - Warmup: indices
..(first + length − 1)areNaNin batch; streaming returnsNoneuntil filled. - Per-step window ignores
NaNs; if a window is entirelyNaN, output isNaNfor 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
AMD Ryzen 9 9950X (CPU) | NVIDIA RTX 4090 (GPU) | Benchmarks: 2026-01-05