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
Improve Your Craft Every Morning
Daily commentary from Dr. Ken Long — what he's seeing in markets, how he's framing trades, and what's worth practicing today. Free.
Your email:
Tue–Fri mornings. Unsubscribe anytime. No spam, no hype.