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

MACD Seasons in JavaScript

Classifies each bar into one of four market-cycle seasons (Spring/Summer/Fall/Winter) from the sign and slope of the MACD of the regression line (MACDRL).

Concept: what MACD-SEASONS means →

Verified. Python and JavaScript implementations agree to season labels match 100% (21/21 comparable bars); underlying MACDRL agrees to 4.26e-14 on a 60-bar reference price series (Canonical Python (edge-transform CoreIndicators.calc_macdseason4 / calc_macdrl) vs JS port (edge-canvas indicators.js calcMACDSeason4 / calcMACDRL), 4-season variant, on the identical 60-bar close. Compared (a) max abs diff of the underlying MACDRL value over the finite region and (b) the fraction of bars where the integer season label matches over the comparable region (bars whose slope predecessor is also finite). The only divergences are warmup-edge bars: Python's polars treats float-NaN warmup as comparable (NaN>0 is True) and emits spurious labels, while the JS port emits null there. That is a NaN-vs-null definedness boundary, not a season-definition difference; the math is identical.).

JavaScript

// Linear-regression endpoint over a trailing window of `n` bars.
// Fits a least-squares line to each window and returns the fitted value at the
// last position. Warmup bars (< n) and any window containing null/NaN are null.
// (matches edge-canvas indicators.js calcRL)
function rlEndpoint(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;
}

// Simple moving average over the last n values; null until a full window of
// finite values is available. (matches edge-canvas indicators.js calcSMA)
function sma(values, n) {
  const len = values.length;
  const result = new Array(len).fill(null);
  if (n <= 0 || len < n) return result;
  let sum = 0, count = 0;
  for (let i = 0; i < len; i++) {
    const v = values[i];
    if (v != null && !Number.isNaN(v)) { sum += v; count++; }
    if (i >= n) {
      const old = values[i - n];
      if (old != null && !Number.isNaN(old)) { sum -= old; count--; }
    }
    if (i >= n - 1 && count === n) result[i] = sum / n;
  }
  return result;
}

// MACD of the regression line: SMA(RL, short) - SMA(RL, long).
// (matches edge-canvas indicators.js calcMACDRL)
function macdRL(closes, short_ = 10, long_ = 30, rlN = 10) {
  const rl = rlEndpoint(closes, rlN);
  const smaShort = sma(rl, short_);
  const smaLong = sma(rl, long_);
  const len = closes.length;
  const result = new Array(len).fill(null);
  for (let i = 0; i < len; i++) {
    if (smaShort[i] != null && smaLong[i] != null) result[i] = smaShort[i] - smaLong[i];
  }
  return result;
}

// 4-season MACD classification from MACDRL sign and slope.
//   1 = Spring (macdrl < 0, rising)   2 = Summer (macdrl > 0, rising)
//   3 = Fall   (macdrl > 0, falling)  4 = Winter (macdrl < 0, falling)
// rising = macdrl >= SMA(macdrl, 2). Returns ints 1-4 or null.
// (matches edge-canvas indicators.js calcMACDSeason4)
function macdSeasons(closes, short_ = 10, long_ = 30, rlN = 10) {
  const macdrl = macdRL(closes, short_, long_, rlN);
  const sma2 = sma(macdrl, 2);
  const len = closes.length;
  const result = new Array(len).fill(null);
  for (let i = 0; i < len; i++) {
    if (macdrl[i] == null || sma2[i] == null) continue;
    const positive = macdrl[i] > 0;
    const rising = macdrl[i] >= sma2[i];
    if (!positive && rising) result[i] = 1;        // Spring
    else if (positive && rising) result[i] = 2;    // Summer
    else if (positive && !rising) result[i] = 3;   // Fall
    else result[i] = 4;                            // Winter
  }
  return result;
}