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

MACD Seasons in Python

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

Python

import numpy as np
import polars as pl


def rl_endpoint(values: np.ndarray, n: int = 10) -> np.ndarray:
    """Linear-regression endpoint over a trailing window of `n` bars.

    Fits a least-squares line to each window [i-n+1 .. i] and returns the
    fitted value at the last position. Warmup bars (< n) are NaN. Vectorized
    via np.correlate with precomputed endpoint weights (matches edge-transform
    CoreIndicators.rl_series).
    """
    values = np.asarray(values, dtype=float)
    length = len(values)
    out = np.full(length, np.nan)
    if n <= 0 or length < n:
        return out
    x = np.arange(n, dtype=float)
    x_mean = (n - 1) / 2.0
    x_diff = x - x_mean
    denom = float(np.sum(x_diff ** 2))
    if denom == 0:
        return out
    # Endpoint weight: intercept term (1/n) + slope contribution at x=n-1.
    w = 1.0 / n + x_mean * x_diff / denom
    out[n - 1:] = np.correlate(values, w, mode="valid")
    return out


def macd_rl(close, short: int = 10, long: int = 30, rl_n: int = 10) -> pl.Series:
    """MACD of the regression line: SMA(RL, short) - SMA(RL, long).

    macdrl = SMA(RL(close, rl_n), short) - SMA(RL(close, rl_n), long)
    (matches edge-transform CoreIndicators.calc_macdrl).
    """
    rl = pl.Series("rl", rl_endpoint(np.asarray(close, dtype=float), n=rl_n))
    sma_short = rl.rolling_mean(short)
    sma_long = rl.rolling_mean(long)
    return (sma_short - sma_long).alias("macdrl")


def macd_seasons(close, short: int = 10, long: int = 30, rl_n: int = 10) -> pl.Series:
    """4-season MACD classification from MACDRL sign and slope.

    Seasons follow a market cycle:
      1 = Spring : macdrl < 0 and rising  (recovery)
      2 = Summer : macdrl > 0 and rising  (expansion)
      3 = Fall   : macdrl > 0 and falling (distribution)
      4 = Winter : macdrl < 0 and falling (contraction)

    Slope is approximated as: rising = macdrl >= SMA(macdrl, 2)
    (matches edge-transform CoreIndicators.calc_macdseason4).
    """
    macdrl = macd_rl(close, short=short, long=long, rl_n=rl_n)
    sma2 = macdrl.rolling_mean(2)
    positive = macdrl > 0
    rising = macdrl >= sma2
    df = pl.DataFrame({"positive": positive, "rising": rising})
    season = df.select(
        pl.when(positive.not_() & rising).then(1)        # Spring
        .when(positive & rising).then(2)                  # Summer
        .when(positive & rising.not_()).then(3)           # Fall
        .when(positive.not_() & rising.not_()).then(4)    # Winter
        .otherwise(None)
        .alias("season")
    )["season"]
    return season