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

PSAR on RL10 in JavaScript

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 →

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).

JavaScript

// 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);
}