# REMA: Regularized Exponential Moving Average

> "Someone looked at the EMA and thought: 'What if we punished it for changing its mind?' The result is REMA—an EMA with a conscience that remembers where it was going and resists the temptation to chase every price wiggle."

REMA (Regularized Exponential Moving Average) combines exponential smoothing with a regularization term that penalizes deviations from the previous trend direction. The result is a filter that responds to genuine price movements while suppressing noise-induced oscillations. Think of it as an EMA with momentum awareness: it knows where it was heading and applies a penalty for sudden course corrections.

## Historical Context

The concept of regularization comes from machine learning and signal processing, where it's used to prevent overfitting by penalizing model complexity. REMA applies this principle to moving averages: the "complexity" being penalized is deviation from the established trend. When price noise tries to yank the average in a new direction, the regularization term pushes back, saying "prove it." The lambda parameter controls how much proof is required—at lambda=1, REMA believes everything (standard EMA); at lambda=0, it's pure momentum that ignores new information entirely.

## Architecture & Physics

REMA introduces a two-component calculation:

1. **EMA Component**: Standard exponential smoothing that responds to new prices
2. **Regularization Component**: Momentum continuation that extrapolates the previous trend

The lambda parameter blends these components:

* **lambda = 1**: Pure EMA behavior. Every price gets full consideration.
* **lambda = 0.5**: Balanced. New prices compete with trend momentum.
* **lambda = 0**: Pure momentum extrapolation. New prices are ignored entirely (not recommended).

The regularization component calculates where the average *would be* if the current trend continued unchanged. The final REMA value is a weighted blend between where EMA wants to go (following price) and where momentum wants to go (continuing trend).

### The Compensator (Warmup Correction)

Like QuanTAlib's EMA implementation, REMA includes a mathematical compensator that corrects for initialization bias. The first N bars aren't approximations—they're mathematically valid from bar one. This means REMA(lambda=1) will match QuanTAlib's EMA implementation exactly, including the bias-corrected warmup period.

## Mathematical Foundation

The standard EMA alpha calculation:

$$ \alpha = \frac{2}{N + 1} $$

The EMA component (standard exponential smoothing):

$$ \text{EMA}_t = \alpha \cdot (P_t - \text{REMA}_{t-1}) + \text{REMA}_{t-1} $$

The regularization component (momentum continuation):

$$ \text{REG}_t = \text{REMA}_{t-1} + (\text{REMA}_{t-1} - \text{REMA}_{t-2}) $$

The final REMA calculation:

$$ \text{REMA}_t = \lambda \cdot (\text{EMA}_t - \text{REG}_t) + \text{REG}_t $$

This can be expanded:

$$ \text{REMA}_t = \lambda \cdot \text{EMA}_t + (1 - \lambda) \cdot \text{REG}_t $$

When $\lambda = 1$: $\text{REMA}_t = \text{EMA}_t$ (standard EMA)

When $\lambda = 0$: $\text{REMA}_t = \text{REG}_t$ (pure momentum extrapolation)

### Bias Compensation

To handle initialization bias, the compensator tracks the sum of weights:

$$ E_t = (1 - \alpha)^t $$

$$ \text{Corrected REMA}_t = \frac{\text{Uncorrected REMA}_t}{1 - E_t} $$

## Performance Profile

### Operation Count (Streaming Mode)

REMA combines EMA with a regularization term that extrapolates trend:

| Operation | Count | Cost (cycles) | Subtotal |
| :--- | :---: | :---: | :---: |
| SUB (Pt - REMAt-1) | 1 | 1 | 1 |
| FMA (EMA update) | 1 | 4 | 4 |
| SUB (momentum: REMAt-1 - REMAt-2) | 1 | 1 | 1 |
| ADD (REG: prev + momentum) | 1 | 1 | 1 |
| SUB (EMA - REG) | 1 | 1 | 1 |
| FMA (λ × diff + REG) | 1 | 4 | 4 |
| **Total (hot)** | **6** | — | **~12 cycles** |

During warmup (bias compensation active):

| Operation | Count | Cost (cycles) | Subtotal |
| :--- | :---: | :---: | :---: |
| MUL (E × decay) | 1 | 3 | 3 |
| SUB (1 - E) | 1 | 1 | 1 |
| DIV (correction) | 1 | 15 | 15 |
| CMP (warmup check) | 1 | 1 | 1 |
| **Warmup overhead** | **4** | — | **~20 cycles** |

**Total during warmup:** ~32 cycles/bar; **Post-warmup:** ~12 cycles/bar.

### Batch Mode (SIMD Analysis)

REMA is inherently recursive due to state dependency on previous two values. SIMD parallelization across bars is not possible:

| Optimization | Benefit |
| :--- | :--- |
| FMA instructions | Already using 2 FMAs per bar |
| State locality | REMA + PrevRema fit in registers |

### Benchmark Results

| Metric | Value | Notes |
| :--- | :--- | :--- |
| **Throughput (Batch)** | ~400 μs / 500K bars | ~0.8 ns/bar |
| **Throughput (Streaming)** | ~2 ns/bar | Single Update() call |
| **Allocations (Hot Path)** | 0 bytes | Verified via BenchmarkDotNet |
| **Complexity** | O(1) | Two FMA operations per bar |
| **State Size** | 48 bytes | REMA, PrevRema, E, flags, counter |

### Quality Metrics

| Quality | Score (1-10) | Notes |
| :--- | :---: | :--- |
| **Accuracy** | 8 | Tracks price well when lambda > 0.5 |
| **Timeliness** | 7 | Regularization adds slight lag vs pure EMA |
| **Smoothness** | 9 | Primary benefit—significantly smoother than EMA |
| **Overshoot** | 3 | Low overshoot due to momentum awareness |

## Usage Examples

```csharp
// Streaming: Process one bar at a time
var rema = new Rema(20, lambda: 0.5);  // 20-period, balanced regularization
foreach (var bar in liveStream)
{
    var result = rema.Update(new TValue(bar.Time, bar.Close));
    Console.WriteLine($"REMA: {result.Value:F2}");
}

// Different lambda values for different behaviors
var smooth = new Rema(20, lambda: 0.3);   // Strong regularization, very smooth
var balanced = new Rema(20, lambda: 0.5); // Balanced (default)
var responsive = new Rema(20, lambda: 0.8); // Weak regularization, more responsive

// When lambda = 1, REMA equals EMA
var asEma = new Rema(20, lambda: 1.0);  // Equivalent to Ema(20)

// Batch processing with Span (zero allocation)
double[] prices = LoadHistoricalData();
double[] remaValues = new double[prices.Length];
Rema.Batch(prices.AsSpan(), remaValues.AsSpan(), period: 20, lambda: 0.5);

// Batch processing with TSeries
var series = new TSeries();
// ... populate series ...
var results = Rema.Batch(series, period: 20, lambda: 0.5);

// Event-driven chaining
var source = new TSeries();
var rema20 = new Rema(source, 20, 0.5);  // Auto-updates when source changes
source.Add(new TValue(DateTime.UtcNow, 100.0));  // REMA updates

// Pre-load with historical data
var rema = new Rema(20, 0.5);
rema.Prime(historicalPrices);  // Ready to process live data immediately
```

## Validation

Validated in `Rema.Validation.Tests.cs`:

| Test | Status | Notes |
| :--- | :---: | :--- |
| **Lambda=1 matches EMA** | ✅ | REMA(period, 1.0) equals EMA(period) |
| **Mode consistency** | ✅ | Batch, Streaming, Span, Eventing all match |
| **Smoothing behavior** | ✅ | Lower lambda produces smoother output |
| **Prime consistency** | ✅ | Prime() produces same results as streaming |

Run validation: `dotnet test --filter "FullyQualifiedName~RemaValidation"`

## Common Pitfalls

1. **Lambda Confusion**: lambda=1 is standard EMA (no regularization), lambda=0 is pure momentum (ignores new prices). Most use cases want something in between. Start with 0.5 and adjust based on your tolerance for lag vs smoothness.

2. **Not a Prediction Tool**: The regularization component extrapolates trend, but REMA is not a forecasting indicator. It's a filter that resists noise. Don't interpret the momentum component as a price prediction.

3. **Comparing to Other Implementations**: REMA isn't standardized across platforms. The formula here matches the PineScript reference implementation. Other platforms may implement "regularized" averages differently.

4. **Over-regularization**: Setting lambda too low (below 0.3) makes REMA extremely laggy and unresponsive. It will miss genuine trend changes. Use lower lambda values only for visualization or as a baseline reference, not for signal generation.

5. **Using REMA(20, 0.5) Like EMA(20)**: Due to regularization, REMA with lambda < 1 will lag behind EMA. If you're replacing an EMA-based strategy, you may need to reduce the period to compensate, or use higher lambda values.

6. **Forgetting `isNew` for Live Data**: When processing live ticks within the same bar, use `Update(value, isNew: false)` to update without advancing state. Use `isNew: true` (default) only when a new bar opens.

## When to Use REMA

REMA is ideal when:
- You need smoother signals than EMA provides
- Noise-induced whipsaws are causing false signals
- You want to maintain trend-following behavior with reduced sensitivity to outliers
- Your strategy benefits from a filter that "commits" to trends

REMA is less suitable when:
- You need maximum responsiveness (use EMA instead)
- You're comparing against external libraries that don't implement REMA
- You need predictable, standardized behavior across platforms