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
A short note from Dr. Ken Long on the craft of trading. One idea to think about and work on that day, drawn from decades of teaching and live trading. Free.
Your email:
Tue–Fri mornings. Unsubscribe anytime. No spam, no hype.