- Updated Bessel, Bilateral, Blma, Butter, Conv, Ema, Kama, LSMA, MAMA, MGDI, SSF, USF, ATR, ADL, and ADOSC documentation to use bullet points for key concepts and features. - Added a new Qodana configuration file for code analysis. - Removed coverage configuration from Quantower.Tests.csproj to streamline testing setup.
7.2 KiB
EMA: Exponential Moving Average
"The EMA exists because traders in the 1960s were tired of their SMA jumping like a caffeinated squirrel every time a price from 20 days ago dropped out of the window. So they invented a filter that remembers everything but cares about nothing old. Brilliant."
EMA (Exponential Moving Average) is the standard by which all other averages are judged. Unlike the SMA, which treats data from 10 days ago with the same reverence as data from 10 seconds ago, the EMA understands that in markets, recency is relevance. It applies an exponentially decaying weight to older prices, reacting faster to new information while never completely forgetting the past. The AK-47 of technical indicators: been around forever, everyone uses it, not fancy, but it works.
Historical Context
The EMA was brought to the financial world to solve the "drop-off effect" of the SMA. Picture this: your 20-day SMA is cruising along, and suddenly an outlier price from exactly 20 days ago drops out of the window. Your average jumps. Your signal fires. Your algorithm buys. The market laughs. By using a recursive formula, the EMA includes all past data in its calculation, with weights diminishing exponentially toward zero. No drop-off, no surprises, no caffeinated squirrel behavior. It is the infinite impulse response (IIR) filter of the trading world.
Architecture & Physics
The EMA is defined by its smoothing factor, \alpha:
- High
\alpha(close to 1): Fast decay, responsive, noisy. Every tick matters. Your signal will fire at shadows. - Low
\alpha(close to 0): Slow decay, smooth, laggy. You'll catch the trend, but you'll also be late to every party.
The relationship between period N and \alpha is: \alpha = \frac{2}{N + 1}. A 10-period EMA has \alpha \approx 0.18. A 100-period EMA has \alpha \approx 0.02. The period is just a human-friendly way to express exponential decay.
The Compensator (Warmup Correction)
Here's where QuanTAlib diverges from the crowd. A standard EMA starts at zero (or seeds with the first price) and takes 3N bars to converge within 5% of its true value. During warmup, you're trading on lies.
QuanTAlib implements a mathematical compensator that corrects for initialization bias. The EMA is statistically valid from bar one. Not approximately valid. Actually valid. This means the first 14 bars of a 10-period EMA will differ from TA-Lib. TA-Lib is wrong. QuanTAlib is correct. File your complaints with the laws of mathematics.
Mathematical Foundation
The standard recursive formula:
\alpha = \frac{2}{N + 1}
\text{EMA}_t = \alpha \cdot P_t + (1 - \alpha) \cdot \text{EMA}_{t-1}
This can be rewritten using fused multiply-add for precision:
\text{EMA}_t = \text{FMA}(\text{EMA}_{t-1}, (1 - \alpha), \alpha \cdot P_t)
Bias Compensation
To handle the initialization bias (where \text{EMA}_0 is unknown), the sum of weights is tracked:
E_t = (1 - \alpha)^t
\text{Corrected EMA}_t = \frac{\text{Uncorrected EMA}_t}{1 - E_t}
This ensures the EMA doesn't lie to you for the first N bars like every other library.
Performance Profile
Benchmarked on Apple M4, .NET 10.0, AdvSIMD, 500,000 bars:
| Metric | Value | Notes |
|---|---|---|
| Throughput (Span) | 381 μs / 500K bars | 0.76 ns/bar, ~1.3B bars/sec |
| Throughput (Streaming) | ~2 ns/bar | Single Update() call |
| Allocations (Hot Path) | 0 bytes | Verified via BenchmarkDotNet |
| Complexity | O(1) | Single FMA operation |
| State Size | 32 bytes | Two doubles (EMA, compensator) |
Comparative Performance
| Library | Time (500K bars) | Allocated | Relative Speed |
|---|---|---|---|
| QuanTAlib (Span) | 381 μs | 0 B | 1.0× (baseline) |
| Tulip | 353 μs | 0 B | 0.93× |
| TA-Lib | 357 μs | 34 B | 0.94× |
| Skender | 10,635 μs | 23.6 MB | 27.9× slower |
QuanTAlib is competitive with C-based libraries (Tulip, TA-Lib) while providing bias-corrected results and zero allocations.
Usage Examples
// Streaming: Process one bar at a time
var ema = new Ema(20); // 20-period EMA
foreach (var bar in liveStream)
{
var result = ema.Update(new TValue(bar.Time, bar.Close));
Console.WriteLine($"EMA: {result.Value:F2}");
}
// Using alpha directly (signal processing style)
var fastEma = new Ema(0.2); // α=0.2, roughly equivalent to period 9
// Batch processing with Span (zero allocation)
double[] prices = LoadHistoricalData();
double[] emaValues = new double[prices.Length];
Ema.Batch(prices.AsSpan(), emaValues.AsSpan(), period: 20);
// Batch processing with TSeries
var series = new TSeries();
// ... populate series ...
var results = Ema.Batch(series, period: 20);
// Event-driven chaining
var source = new TSeries();
var ema20 = new Ema(source, 20); // Auto-updates when source changes
var ema50 = new Ema(source, 50); // Multiple indicators on same source
source.Add(new TValue(DateTime.UtcNow, 100.0)); // Both EMAs update
// Pre-load with historical data
var ema = new Ema(20);
ema.Prime(historicalPrices); // Ready to process live data immediately
Validation
Validated against external libraries in Ema.Validation.Tests.cs. Tests run against 5,000 bars with tolerance of 1e-9:
| Library | Batch | Streaming | Span | Notes |
|---|---|---|---|---|
| TA-Lib | ✅ | ✅ | ✅ | Matches after warmup period (TA-Lib lacks compensator) |
| Skender | ✅ | ✅ | ✅ | Matches GetEma() |
| Tulip | ✅ | ✅ | ✅ | Matches ema indicator |
| Ooples | ✅ | — | — | Matches CalculateExponentialMovingAverage() |
Run validation: dotnet test --filter "FullyQualifiedName~EmaValidation"
Common Pitfalls
-
The "First Value" Problem: Most libraries seed the EMA with the first price or an SMA of the first
Nprices. Results during warmup are approximations. QuanTAlib uses a mathematical compensator, so early values will differ from TA-Lib. This is not a bug. QuanTAlib is correct; TA-Lib is approximating. -
Alpha vs. Period Confusion:
N=10gives\alpha \approx 0.18. But\alpha=0.1givesN \approx 19. Don't confuse "EMA(10)" (fast, period-based) with "EMA(0.1)" (slow, alpha-based). The constructors accept both, and they are not equivalent. -
EMA Still Lags: The EMA is faster than SMA, but it's not magic. It still lags. A 20-period EMA lags roughly 10 bars behind price. If you want zero lag, you want a Jurik Moving Average (JMA) or Ehlers filters. But those have their own problems.
-
Using EMA(5) on Hourly Data: An EMA(5) on hourly bars has a half-life of about 2.5 hours. Every minor wiggle becomes a signal. Your trading bot will panic-trade its way to bankruptcy. Use longer periods on longer timeframes.
-
Expecting Identical Results Across Libraries: During the first
Nbars, QuanTAlib will differ from TA-Lib, Tulip, and Skender due to bias compensation. After warmup, all libraries converge. If you're comparing results, skip the first3Nbars. -
Forgetting
isNewfor Live Data: When processing live ticks within the same bar, useUpdate(value, isNew: false)to update without advancing state. UseisNew: true(default) only when a new bar opens. Getting this wrong causes your EMA to runNtimes faster than intended.