OGT Owl Group Trading by Dr. Ken Long
Home About Learn The Loop Code Courses Essays Store Partners FAQ
Indicator Code · Owl Group Trading

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.

Verified. Python and JavaScript implementations agree to 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);
}