Arnaud Legoux Moving Average (ALMA)

Haiyue
11min

I. What is ALMA

The Arnaud Legoux Moving Average (ALMA) is an advanced moving average that uses a Gaussian distribution (normal distribution) as its weight function. It was co-invented by French engineer Arnaud Legoux and Greek mathematician Dimitris Kouzis-Loukas, and was first published around 2009.

Historical Background

Legoux and Kouzis-Loukas drew inspiration from the signal processing domain, arguing that the ideal moving average weight function should be a bell curve (Gaussian function), rather than a linear decay (WMA) or exponential decay (EMA). Gaussian weights offer the following advantages:

  1. The center region carries the highest weight, with rapid decay on both sides
  2. The weight curve is extremely smooth, with no hard cutoff
  3. By adjusting the offset parameter, the weight center can be positioned anywhere within the window

ALMA allows users to fine-tune the weight distribution through offset and sigma parameters, achieving flexible control over the trade-off between lag and smoothness.

Indicator Classification

  • Type: Overlay indicator, plotted on the price chart
  • Category: Moving Averages
  • Default Parameters: Period n=20n = 20, offset =0.85= 0.85, sigma =6= 6, data source is closing price (Close)

II. Mathematical Principles and Calculation

Core Formula

ALMA weights are based on the Gaussian function:

wi=exp((im)22s2)w_i = \exp\left(-\frac{(i - m)^2}{2s^2}\right)

Where:

  • i=0,1,2,,n1i = 0, 1, 2, \ldots, n-1 (index within the window, 0 being the oldest data)
  • m=offset×(n1)m = offset \times (n - 1) (center position of the Gaussian bell curve)
  • s=nsigmas = \frac{n}{sigma} (standard deviation of the Gaussian function, controlling the bell width)

The ALMA calculation formula is:

ALMAt=i=0n1wiPtn+1+ii=0n1wiALMA_t = \frac{\sum_{i=0}^{n-1} w_i \cdot P_{t-n+1+i}}{\sum_{i=0}^{n-1} w_i}

Parameter Details

offset (range 0 ~ 1, default 0.85)

Controls the position of the Gaussian bell center within the window:

  • offset=0.5offset = 0.5: Weight center at the window midpoint; maximum lag but smoothest
  • offset=1.0offset = 1.0: Weight center at the newest data point; minimum lag but less smooth
  • offset=0.85offset = 0.85 (default): Biased toward recent data; a good balance between lag and smoothness

sigma (width parameter, default 6)

Controls the width of the Gaussian bell:

  • Smaller sigmasigma: Narrower bell, weights more concentrated near the center, behaves like a short-period average
  • Larger sigmasigma: Wider bell, more uniformly distributed weights, behaves like SMA
Intuition Behind Gaussian Weights

Imagine a bell curve placed over the last 20 days of prices. offset=0.85 means the bell’s peak (highest weight) is at approximately day 17 (i.e., the 3rd most recent day), very close to the current price but not exactly at the latest day. This allows ALMA to be both responsive and resistant to latest-day noise.

Numerical Example

For n=5n = 5, offset=0.85offset = 0.85, sigma=6sigma = 6:

  • m=0.85×(51)=3.4m = 0.85 \times (5 - 1) = 3.4
  • s=5/60.833s = 5 / 6 \approx 0.833

Weights for each index:

ii(im)2/(2s2)(i - m)^2 / (2s^2)wiw_i
0(03.4)2/1.389=8.324(0 - 3.4)^2 / 1.389 = 8.3240.000
1(13.4)2/1.389=4.147(1 - 3.4)^2 / 1.389 = 4.1470.016
2(23.4)2/1.389=1.411(2 - 3.4)^2 / 1.389 = 1.4110.244
3(33.4)2/1.389=0.115(3 - 3.4)^2 / 1.389 = 0.1150.891
4(43.4)2/1.389=0.259(4 - 3.4)^2 / 1.389 = 0.2590.772

As shown, weight is concentrated at i=3i = 3 and i=4i = 4 (the two most recent days), consistent with the offset=0.85 bias.


III. Python Implementation

import numpy as np
import pandas as pd

def alma(close: pd.Series, period: int = 20,
         offset: float = 0.85, sigma: float = 6.0) -> pd.Series:
    """
    Calculate the Arnaud Legoux Moving Average (ALMA)

    Parameters
    ----------
    close : pd.Series
        Closing price series
    period : int
        Calculation period, default is 20
    offset : float
        Gaussian weight center offset (0~1), default is 0.85
    sigma : float
        Gaussian width parameter, default is 6

    Returns
    -------
    pd.Series
        ALMA value series
    """
    m = offset * (period - 1)
    s = period / sigma

    # Calculate Gaussian weights
    weights = np.array([
        np.exp(-((i - m) ** 2) / (2 * s * s))
        for i in range(period)
    ])
    weights /= weights.sum()  # Normalize

    def _alma(window):
        return np.dot(window, weights)

    return close.rolling(window=period, min_periods=period).apply(_alma, raw=True)


def alma_numpy(close: np.ndarray, period: int = 20,
               offset: float = 0.85, sigma: float = 6.0) -> np.ndarray:
    """
    Manual ALMA implementation using numpy
    """
    n = len(close)
    result = np.full(n, np.nan)

    m = offset * (period - 1)
    s = period / sigma

    # Construct Gaussian weights
    idx = np.arange(period)
    weights = np.exp(-((idx - m) ** 2) / (2 * s * s))
    weights /= weights.sum()

    for i in range(period - 1, n):
        window = close[i - period + 1 : i + 1]
        result[i] = np.dot(window, weights)

    return result


# ========== Usage Example ==========
if __name__ == "__main__":
    np.random.seed(42)
    dates = pd.date_range("2024-01-01", periods=150, freq="D")
    price = 100 + np.cumsum(np.random.randn(150) * 0.8)

    df = pd.DataFrame({
        "date": dates,
        "open":  price + np.random.randn(150) * 0.3,
        "high":  price + np.abs(np.random.randn(150) * 0.6),
        "low":   price - np.abs(np.random.randn(150) * 0.6),
        "close": price,
        "volume": np.random.randint(1000, 10000, size=150),
    })
    df.set_index("date", inplace=True)

    # Compare ALMA with different offsets
    df["EMA_20"]  = df["close"].ewm(span=20, adjust=False).mean()
    df["ALMA_default"] = alma(df["close"], 20, offset=0.85, sigma=6)
    df["ALMA_fast"]    = alma(df["close"], 20, offset=0.95, sigma=6)
    df["ALMA_smooth"]  = alma(df["close"], 20, offset=0.50, sigma=6)

    print("=== ALMA Comparison with Different Offsets ===")
    print(df[["close", "EMA_20", "ALMA_default", "ALMA_fast", "ALMA_smooth"]].tail(10))

    # Compare different sigma values
    df["ALMA_s3"] = alma(df["close"], 20, offset=0.85, sigma=3)
    df["ALMA_s6"] = alma(df["close"], 20, offset=0.85, sigma=6)
    df["ALMA_s12"] = alma(df["close"], 20, offset=0.85, sigma=12)

    print("\n=== ALMA Comparison with Different Sigma Values ===")
    print(df[["close", "ALMA_s3", "ALMA_s6", "ALMA_s12"]].tail(10))

    # Crossover signal
    df["signal"] = np.where(df["close"] > df["ALMA_default"], 1, -1)
    df["cross"] = df["signal"].diff().abs() > 0
    print("\n=== ALMA Crossover Signals ===")
    print(df.loc[df["cross"], ["close", "ALMA_default", "signal"]])

IV. Problems the Indicator Solves

1. Flexible Lag-Smoothness Trade-off

ALMA’s most unique capability is the fine-grained control over lag and smoothness through the offset and sigma parameters. This is something other moving averages cannot achieve:

  • Increase offset -> Reduce lag
  • Increase sigma -> Increase smoothness

Traders can freely adjust these according to different market conditions and strategy requirements.

2. Eliminating Window Boundary Effects

Gaussian weights naturally decay to near zero at both ends of the window, avoiding the hard cutoff problem seen in SMA and WMA. When price data exits the window, ALMA’s value does not experience abrupt jumps.

3. Signal-Processing-Grade Smoothing

ALMA’s Gaussian kernel function is directly related to low-pass filters in signal processing. Its suppression of high-frequency noise in the frequency domain is superior to linear or exponential weighting schemes.

4. Reducing False Crossovers

Due to the smooth nature of Gaussian weights, ALMA produces fewer false crossover signals than same-period EMA, performing more stably especially in ranging markets.

Parameter Tuning Suggestions
  • Clear trends: offset = 0.85~0.95, sigma = 6, biased toward fast response
  • Ranging markets: offset = 0.50.7, sigma = 610, biased toward smoothness
  • Long-term investing: offset = 0.5, sigma = 10+, similar to a smoother SMA

V. Advantages, Disadvantages, and Use Cases

Advantages

AdvantageDescription
Flexible parametersoffset and sigma provide fine-grained lag/smoothness control
No hard cutoffGaussian weights decay naturally; no window boundary jumps
High smoothnessGaussian kernel smoothing outperforms linear/exponential weights
Theoretically rigorousBased on well-established Gaussian filtering theory from signal processing

Disadvantages

DisadvantageDescription
Too many parametersThree parameters (period, offset, sigma) increase tuning complexity
Higher computationEach calculation requires a weighted sum over the entire window
High learning barrierThe Gaussian distribution concept is not intuitive for non-mathematical traders
Non-mainstreamUncommon in traditional technical analysis textbooks; may lack shared understanding

Use Cases

  • Quantitative strategy development: Research requiring fine control over moving average behavior
  • Signal-processing-oriented analysis: Optimizing price filtering from a frequency-domain perspective
  • Multi-market adaptation: Using different offset/sigma parameters for different markets

Comparison with Similar Indicators

FeatureSMAEMAWMAALMA
Weight functionConstantExponentialLinearGaussian
Adjustable parameters1113
Boundary effectsSevereNoneModerateNone
SmoothnessMediumMediumMediumHigh
FlexibilityLowLowLowHigh
Notes
  1. Do not blindly increase offset (close to 1.0), as this makes ALMA focus almost exclusively on the last one or two days, losing its smoothing function.
  2. Setting sigma to very small values (e.g., 1-2) causes ALMA to degenerate to looking at only a few prices near the window center; this is not recommended.
  3. When backtesting ALMA strategies, all three parameters should be optimized simultaneously. Be aware of overfitting risk and use cross-validation or out-of-sample testing.