PSAR on RL10
A Wilder Parabolic SAR trailing stop walked over the smoothed RL10 regression line instead of raw price, so the stop reacts to trend rather than to single-bar noise.
Concept: what PSAR-ON-RL10 means →
PSAR on RL10 first computes the RL10 series — the 10-bar linear-regression endpoint at each bar — then runs a standard Wilder Parabolic SAR (af=0.02, max=0.12) over that smoothed line rather than over raw OHLC. Because RL10 filters out single-bar spikes, the resulting parabolic trailing stop flips on sustained regression-line reversals instead of intrabar noise, giving a cleaner trend-following stop. The acceleration factor steps by 0.02 on each new extreme point and caps at 0.12; on a flip the SAR jumps to the prior extreme point and the trend reverses.
4.26e-14 on a 60-bar reference price series (RL10 then PSAR) (canonical Python psar_walk_series vs JS calcPSAROnSeries on the RL10 series).Pythonpermalink →
import numpy as np
def rl_series(values, n=10):
"""RL10: linear-regression endpoint at the last bar of each n-bar window.
Returns a float array the same length as `values`; the first n-1 entries
are NaN (insufficient lookback). Vectorized via np.correlate.
Canonical: edge-transform CoreIndicators.rl_series (contract HR-007).
"""
values = np.asarray(values, dtype=float)
window = int(n)
length = len(values)
out = np.full(length, np.nan, dtype=float)
if window <= 0 or length < window:
return out
x = np.arange(window, dtype=float)
x_mean = (window - 1) / 2.0
x_diff = x - x_mean
denom = float(np.sum(x_diff ** 2))
if denom == 0:
return out
# Fixed weights whose dot-product over the window equals the regression
# endpoint value (slope*(n-1) + intercept) at x = n-1.
w = 1.0 / window + x_mean * x_diff / denom
out[window - 1:] = np.correlate(values, w, mode="valid")
return out
def psar_walk_series(values, af=0.02, m=0.12):
"""Wilder Parabolic SAR forward walk over a 1-D series.
NaN warmup bars are skipped; the walk starts at the first index i where
values[i] and values[i+1] are both finite. Returns the SAR array.
Canonical: edge-transform CoreIndicators.psar_walk_series (contract HR-006).
"""
n = len(values)
psar_vals = np.full(n, np.nan)
if n < 2:
return psar_vals
start = -1
for i in range(n - 1):
if not np.isnan(values[i]) and not np.isnan(values[i + 1]):
start = i
break
if start < 0:
return psar_vals
tr = 1 if values[start + 1] >= values[start] else -1
ep = float(values[start + 1]) # extreme point
accel = af # acceleration factor
psar_vals[start] = psar_vals[start + 1] = float(values[start])
for i in range(start + 2, n):
if np.isnan(values[i]):
continue
prev = psar_vals[i - 1]
if tr == 1:
ps = prev + accel * (ep - prev)
# SAR may not penetrate the prior one/two bars.
if not np.isnan(values[i - 1]):
ps = min(ps, float(values[i - 1]))
if i >= 3 and not np.isnan(values[i - 2]):
ps = min(ps, float(values[i - 2]))
if values[i] < ps: # flip to short
tr = -1
ps = ep
ep = float(values[i])
accel = af
elif values[i] > ep: # new high -> step accel
ep = float(values[i])
accel = min(accel + af, m)
else:
ps = prev + accel * (ep - prev)
if not np.isnan(values[i - 1]):
ps = max(ps, float(values[i - 1]))
if i >= 3 and not np.isnan(values[i - 2]):
ps = max(ps, float(values[i - 2]))
if values[i] > ps: # flip to long
tr = 1
ps = ep
ep = float(values[i])
accel = af
elif values[i] < ep: # new low -> step accel
ep = float(values[i])
accel = min(accel + af, m)
psar_vals[i] = ps
return psar_vals
def psar_on_rl10(close, af=0.02, m=0.12):
"""Parabolic SAR computed over the RL10 line of `close`.
1. Smooth price into the RL10 regression-endpoint series.
2. Walk a Wilder PSAR (af=0.02, max=0.12) over that smoothed series.
Returns a NaN-padded SAR array aligned to `close`.
"""
rl = rl_series(close, n=10)
return psar_walk_series(rl, af=af, m=m)
JavaScriptpermalink →
// RL10: linear-regression endpoint at the last bar of each n-bar window.
// Null for the first n-1 bars (insufficient lookback). Canonical:
// edge-canvas/static/indicators.js::calcRL.
function calcRL(closes, n = 10) {
const len = closes.length;
const result = new Array(len).fill(null);
if (n <= 0 || len < n) return result;
const sumX = (n * (n - 1)) / 2;
const sumX2 = (n * (n - 1) * (2 * n - 1)) / 6;
const denom = n * sumX2 - sumX * sumX;
if (denom === 0) return result;
for (let end = n; end <= len; end++) {
const start = end - n;
let sumY = 0, sumXY = 0, hasNaN = false;
for (let j = 0; j < n; j++) {
const v = closes[start + j];
if (v == null || Number.isNaN(v)) { hasNaN = true; break; }
sumY += v; sumXY += j * v;
}
if (hasNaN) continue;
const slope = (n * sumXY - sumX * sumY) / denom;
const intercept = (sumY - slope * sumX) / n;
result[end - 1] = slope * (n - 1) + intercept;
}
return result;
}
// Wilder Parabolic SAR over an arbitrary 1-D series (here the RL10 line).
// Null entries are skipped; walk starts at the first two consecutive
// non-null values. Canonical: edge-canvas/static/indicators.js::calcPSAROnSeries.
function calcPSAROnSeries(series, afInit = 0.02, afMax = 0.12) {
const len = series.length;
const result = new Array(len).fill(null);
let start = -1;
for (let i = 0; i < len - 1; i++) {
if (series[i] != null && series[i + 1] != null) { start = i; break; }
}
if (start < 0) return result;
let trend = series[start + 1] >= series[start] ? 1 : -1;
let ep = series[start + 1]; // extreme point
let af = afInit; // acceleration factor
result[start] = series[start];
result[start + 1] = series[start];
for (let i = start + 2; i < len; i++) {
if (series[i] == null) continue;
const prev = result[i - 1];
let p;
if (trend === 1) {
p = prev + af * (ep - prev);
// SAR may not penetrate the prior one/two bars.
if (i >= 2 && series[i - 1] != null) p = Math.min(p, series[i - 1]);
if (i >= 3 && series[i - 2] != null) p = Math.min(p, series[i - 2]);
if (series[i] < p) { // flip to short
trend = -1; p = ep; ep = series[i]; af = afInit;
} else if (series[i] > ep) { // new high -> step accel
ep = series[i]; af = Math.min(af + afInit, afMax);
}
} else {
p = prev + af * (ep - prev);
if (i >= 2 && series[i - 1] != null) p = Math.max(p, series[i - 1]);
if (i >= 3 && series[i - 2] != null) p = Math.max(p, series[i - 2]);
if (series[i] > p) { // flip to long
trend = 1; p = ep; ep = series[i]; af = afInit;
} else if (series[i] < ep) { // new low -> step accel
ep = series[i]; af = Math.min(af + afInit, afMax);
}
}
result[i] = p;
}
return result;
}
// Parabolic SAR computed over the RL10 line of `closes`.
// 1. Smooth price into the RL10 regression-endpoint series.
// 2. Walk a Wilder PSAR (af=0.02, max=0.12) over that smoothed series.
export function psar_on_rl10(closes, afInit = 0.02, afMax = 0.12) {
const rl = calcRL(closes, 10);
return calcPSAROnSeries(rl, afInit, afMax);
}
Improve Your Craft Every Morning
Daily commentary from Dr. Ken Long — what he's seeing in markets, how he's framing trades, and what's worth practicing today. Free.
Your email:
Tue–Fri mornings. Unsubscribe anytime. No spam, no hype.