Files
2026-02-23 17:27:35 -08:00

86 lines
3.8 KiB
Plaintext

// This Pine Script™ code is subject to the terms of the Mozilla Public License 2.0
// https://mozilla.org/MPL/2.0/
// © QuanTAlib
//@version=6
indicator("SAM: Smoothed Adaptive Momentum", shorttitle="SAM", overlay=false)
// @function Calculates the Ehlers Smoothed Adaptive Momentum.
// Measures the Dominant Cycle period via Homodyne Discriminator,
// then computes one-cycle momentum (close - close[DC]) and applies
// a 2-pole Super Smoother filter for final output.
// Source: John F. Ehlers, "Cybernetic Analysis for Stocks and Futures" (2004),
// Chapter 12: "Adapting to the Trend," p.166.
// @param src Series to analyze.
// @param alpha Smoothing factor for cycle measurement. Default 0.07.
// @param cutoff Super Smoother cutoff period. Default 8.
// @returns The smoothed adaptive momentum oscillator value.
sam(series float src, simple float alpha, simple int cutoff) =>
// ── 4-bar FIR smoother ──
float smooth = (src + 2.0 * nz(src[1]) + 2.0 * nz(src[2]) + nz(src[3])) / 6.0
// ── Hilbert Transform via Ehlers' detrender/quadrature ──
float pi = math.pi
float detrend = 0.0
detrend := (0.0962 * smooth + 0.5769 * nz(smooth[2]) - 0.5769 * nz(smooth[4]) - 0.0962 * nz(smooth[6])) * (0.075 * nz(detrend[1]) + 0.54)
// ── In-phase and Quadrature components ──
float q1 = 0.0
q1 := (0.0962 * detrend + 0.5769 * nz(detrend[2]) - 0.5769 * nz(detrend[4]) - 0.0962 * nz(detrend[6])) * (0.075 * nz(q1[1]) + 0.54)
float i1 = nz(detrend[3])
// ── Advance phase by 90 degrees ──
float ji = (0.0962 * i1 + 0.5769 * nz(i1[2]) - 0.5769 * nz(i1[4]) - 0.0962 * nz(i1[6])) * (0.075 * nz(ji[1]) + 0.54)
float jq = (0.0962 * q1 + 0.5769 * nz(q1[2]) - 0.5769 * nz(q1[4]) - 0.0962 * nz(q1[6])) * (0.075 * nz(jq[1]) + 0.54)
// ── Phasor addition for Homodyne Discriminator ──
float i2 = 0.0
float q2 = 0.0
i2 := i1 - jq
q2 := q1 + ji
i2 := alpha * i2 + (1.0 - alpha) * nz(i2[1])
q2 := alpha * q2 + (1.0 - alpha) * nz(q2[1])
// ── Homodyne Discriminator for period ──
float re = 0.0
float im = 0.0
re := i2 * nz(i2[1]) + q2 * nz(q2[1])
im := i2 * nz(q2[1]) - q2 * nz(i2[1])
re := alpha * re + (1.0 - alpha) * nz(re[1])
im := alpha * im + (1.0 - alpha) * nz(im[1])
float period = 0.0
if im != 0.0 and re != 0.0
period := 2.0 * pi / math.atan(im / re)
period := math.max(math.min(period, 50.0), 6.0)
float instPeriod = 0.0
instPeriod := 0.33 * period + 0.67 * nz(instPeriod[1])
float dcPeriod = 0.0
dcPeriod := 0.15 * instPeriod + 0.85 * nz(dcPeriod[1])
// ── Adaptive Momentum: one dominant-cycle lookback ──
int dcLen = math.max(int(dcPeriod), 1)
float momentum = src - nz(src[dcLen])
// ── 2-pole Super Smoother on momentum ──
float a1 = math.exp(-math.sqrt(2.0) * pi / cutoff)
float b1 = 2.0 * a1 * math.cos(math.sqrt(2.0) * pi / cutoff)
float c2 = b1
float c3 = -a1 * a1
float c1 = 1.0 - c2 - c3
float filt = 0.0
filt := c1 * (momentum + nz(momentum[1])) / 2.0 + c2 * nz(filt[1]) + c3 * nz(filt[2])
filt
// ── Inputs ──────────────────────────────────────────────
a = input.float(0.07, "Alpha", minval=0.01, maxval=1.0, step=0.01)
c = input.int(8, "Cutoff", minval=2)
// ── Calculation ─────────────────────────────────────────
result = sam(close, a, c)
// ── Plot ────────────────────────────────────────────────
plot(result, "SAM", color=color.yellow, linewidth=2)
hline(0, "Zero", color=color.gray, linestyle=hline.style_dotted)