mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-23 04:58:08 +00:00
124 lines
6.0 KiB
Markdown
124 lines
6.0 KiB
Markdown
# ALMA: Arnaud Legoux Moving Average
|
|
|
|
> "Gaussian distributions govern everything from particle diffusion to the distribution of shoe sizes. Applying them to price action isn't 'technical analysis'; it's just physics with a profit motive."
|
|
|
|
ALMA is a Finite Impulse Response (FIR) filter that applies a Gaussian window to price data. Unlike the Simple Moving Average (which treats 10-minute-old data with the same reverence as 1-minute-old data) or the Exponential Moving Average (which holds onto history like a hoarder), ALMA allows you to shape the weight distribution precisely. It lets you define the trade-off between smoothness and lag using standard deviation ($\sigma$) and offset, rather than arbitrary periods.
|
|
|
|
## Historical Context / The Standard
|
|
|
|
Arnaud Legoux and Dimitris Kouzis-Loukas published ALMA in 2009. The context was a trading world drowning in "adaptive" moving averages (KAMA, FRAMA) that often adapted too late or overshot the turn.
|
|
|
|
While Hull (HMA) attempted to solve lag through algebraic subtraction (and created overshoot), and Jurik (JMA) hid behind proprietary black-box math, Legoux returned to first principles: Signal Processing. He applied the Gaussian filter—standard in electrical engineering for noise reduction—to financial time series. It is not a "modern" invention so much as the correct application of established math to a messy domain.
|
|
|
|
## Architecture & Physics
|
|
|
|
ALMA is a weighted moving average where weights follow a normal distribution (bell curve).
|
|
|
|
The physics of ALMA rely on shifting the "center of gravity" of the window.
|
|
|
|
- **SMA:** Center of gravity is always the middle ($0.5$). Lag is fixed.
|
|
- **EMA:** Center of gravity is front-loaded but has an infinite tail.
|
|
- **ALMA:** You move the center. An offset of $0.85$ pushes the bulk of the weight to the most recent 15% of the window.
|
|
|
|
This shift allows the indicator to capture momentum (high responsiveness) while the Gaussian decay kills high-frequency noise (smoothness). It behaves less like a lagging indicator and more like a mass-dampener system.
|
|
|
|
### The Compute Challenge
|
|
|
|
Naive implementations recalculate the Gaussian weights on every tick. This is CPU suicide.
|
|
QuanTAlib precomputes the weight vector $\mathbf{W}$ upon initialization. The runtime operation effectively becomes a dot product of the price buffer and the weight vector.
|
|
|
|
$$ \text{Runtime Cost} = O(N) \text{ multiplications} $$
|
|
|
|
While heavier than the recursive EMA ($O(1)$), the memory locality of the arrays allows modern CPUs to vectorise these operations (SIMD), making the penalty negligible for typical window sizes (< 100).
|
|
|
|
## Mathematical Foundation
|
|
|
|
The weight calculation relies on three inputs:
|
|
|
|
1. **Window ($L$)**: The lookback period.
|
|
2. **Offset ($o$)**: Where the Gaussian peak sits (0.0 to 1.0). Default is 0.85.
|
|
3. **Sigma ($\sigma$)**: The width of the bell curve. Default is 6.0.
|
|
|
|
### 1. Center and Width Calculation
|
|
|
|
First, QuanTAlib defines the peak index ($m$) and the spread ($s$):
|
|
|
|
$$ m = o \cdot (L - 1) $$
|
|
|
|
$$ s = \frac{L}{\sigma} $$
|
|
|
|
### 2. Weight Generation
|
|
|
|
For each index $i$ from $0$ to $L-1$, the unnormalized weight is calculated:
|
|
|
|
$$ w_i = \exp \left( - \frac{(i - m)^2}{2s^2} \right) $$
|
|
|
|
### 3. Normalization
|
|
|
|
The final ALMA value is the weighted sum. The weights are not normalized to sum to 1.0 beforehand; instead, division by the total sum of weights $W_{sum}$ happens at the end.
|
|
|
|
$$ \text{ALMA}_t = \frac{\sum_{i=0}^{L-1} P_{t-i} \cdot w_{L-1-i}}{W_{sum}} $$
|
|
|
|
*Note: The weights vector is reversed relative to the price history buffer (most recent price gets the weight at the offset index).*
|
|
|
|
## Performance Profile
|
|
|
|
ALMA trades a small amount of CPU cycles for superior signal fidelity.
|
|
|
|
| Metric | Score | Notes |
|
|
| :--- | :--- | :--- |
|
|
| **Throughput** | 35ns/bar | Slower than EMA (5ns), faster than sorting-based medians. |
|
|
| **Allocations** | 0 | Weights precomputed. Buffer is circular. |
|
|
| **Complexity** | $O(N)$ | Linear with window size. Vectorizable. |
|
|
| **Accuracy** | 10/10 | Matches Gaussian definition to `double` precision. |
|
|
| **Timeliness** | 9/10 | Tunable offset (0.85) minimizes group delay. |
|
|
| **Overshoot** | 9/10 | Gaussian decay prevents the "whip" effect of HMA. |
|
|
| **Smoothness** | 8/10 | Dependent on $\sigma$; higher $\sigma$ = sharper filter. |
|
|
|
|
### Implementation Details
|
|
|
|
```csharp
|
|
// Precomputation (Constructor)
|
|
double m = offset * (period - 1);
|
|
double s = period / sigma;
|
|
double wSum = 0;
|
|
|
|
for (int i = 0; i < period; i++) {
|
|
double weight = Math.Exp(-((i - m) * (i - m)) / (2 * s * s));
|
|
_weights[i] = weight;
|
|
wSum += weight;
|
|
}
|
|
|
|
// Runtime (Update)
|
|
double numerator = 0;
|
|
// Note: _buffer holds prices. _weights are pre-aligned.
|
|
// Modern JIT unrolls this loop efficiently.
|
|
for (int i = 0; i < period; i++) {
|
|
numerator += _buffer[i] * _weights[i];
|
|
}
|
|
return numerator / wSum;
|
|
```
|
|
|
|
## Validation
|
|
|
|
QuanTAlib validates against reference implementations that respect the Gaussian math, ignoring those that approximate for speed.
|
|
|
|
| Library | Status | Notes |
|
|
| :--- | :--- | :--- |
|
|
| **QuanTAlib** | ✅ | Validated against math definition. |
|
|
| **Skender** | ✅ | Matches `GetAlma`. |
|
|
| **Ooples** | ✅ | Matches `CalculateArnaudLegouxMovingAverage`. |
|
|
| **Pandas-TA** | ✅ | Python reference implementation matches. |
|
|
| **TA-Lib** | ❌ | Not included in standard C distribution. |
|
|
| **Tulip** | ❌ | Not included. |
|
|
|
|
## Common Pitfalls
|
|
|
|
1. **Offset Abuse**: Setting offset to `0.99` creates a filter that barely filters. It tracks price so closely you might as well use `Price[0]`. Setting it to `0.5` makes it a centered moving average (great for smoothing, terrible for trading due to repainting if used as such, but ALMA does not repaint). The magic is in the `0.85` region.
|
|
|
|
2. **Sigma Confusion**:
|
|
- $\sigma = 1$: The curve is flat. You have reinvented the Simple Moving Average (badly).
|
|
- $\sigma = 10$: The curve is a needle. You are sampling one specific bar in history.
|
|
|
|
3. **Cold Start**: ALMA requires a full window ($L$) to be mathematically valid. First $L-1$ bars are convergence noise. Ignore them.
|