7.5 KiB
FRAMA: Ehlers Fractal Adaptive Moving Average
| Property | Value |
|---|---|
| Category | Trend (IIR MA) |
| Inputs | OHLCV bar (TBar) |
| Parameters | period |
| Outputs | Single series (Frama) |
| Output range | Tracks input |
| Warmup | pe bars |
| Signature | frama_signature |
TL;DR
- FRAMA is John Ehlers' fractal adaptive moving average.
- Parameterized by
period. - Output range: Tracks input.
- Requires
pebars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available.
"Markets do not move at one speed. FRAMA listens to the roughness and adjusts the filter."
FRAMA is John Ehlers' fractal adaptive moving average. It estimates a fractal dimension from high and low ranges, then converts that dimension into a dynamic EMA alpha. The result is a moving average that tightens in trends and relaxes in noise.
Historical Context
FRAMA was introduced in Traders' Tips as an adaptive filter that uses fractal geometry as a proxy for market roughness. It is a classic Ehlers indicator and remains a reference point for adaptive smoothing.
Architecture & Physics
FRAMA splits the window into two halves, compares the combined range to the full range, and derives a fractal dimension:
- Compute ranges over the first half, second half, and full window.
- Convert range ratios to a dimension estimate.
- Convert dimension to a dynamic alpha.
- Apply EMA smoothing to HL2 using that alpha.
The implementation follows the strict Ehlers definition:
- Range windows use High and Low, not Close.
- Smoothed price is HL2.
- Period is forced even.
- Alpha is clamped to [0.01, 1.0].
Math Foundation
Let N be even, h = N/2. Ranges are:
N_1 = \frac{\max(\text{High}_{t-h+1..t}) - \min(\text{Low}_{t-h+1..t})}{h}
N_2 = \frac{\max(\text{High}_{t-2h+1..t-h}) - \min(\text{Low}_{t-2h+1..t-h})}{h}
N_3 = \frac{\max(\text{High}_{t-2h+1..t}) - \min(\text{Low}_{t-2h+1..t})}{N}
Fractal dimension:
D = \frac{\ln(N_1 + N_2) - \ln(N_3)}{\ln(2)}
Alpha and update:
\alpha = \exp(-4.6 \cdot (D - 1))
\alpha = \min(1, \max(0.01, \alpha))
FRAMA_t = \alpha \cdot HL2_t + (1-\alpha) \cdot FRAMA_{t-1}
Performance Profile
Operation Count (Streaming Mode, Scalar)
Hot path (buffer full, period=20):
| Operation | Count | Cost (cycles) | Subtotal |
|---|---|---|---|
| CMP | 3×N | 1 | 60 |
| ADD/SUB | 6 | 1 | 6 |
| DIV | 3 | 15 | 45 |
| LOG | 2 | 40 | 80 |
| EXP | 1 | 50 | 50 |
| MUL | 2 | 3 | 6 |
| FMA | 1 | 4 | 4 |
| Total | — | — | ~251 cycles |
The hot path consists of:
- HL2 price:
(high + low) * 0.5— 1 ADD + 1 MUL - Range scans (3 windows): min/max over N, N/2, N/2 — 3×N CMP (60 for period=20)
- Range normalization: 3 DIV operations
- Fractal dimension:
(ln(N1+N2) - ln(N3)) / ln(2)— 2 LOG + 1 ADD + 1 SUB + 1 DIV - Alpha calculation:
exp(-4.6 * (D - 1))— 1 EXP + 1 MUL + 1 SUB - EMA update:
FMA(prev, 1-alpha, alpha * price)— 1 FMA + 1 MUL
Complexity note: Range scans are O(N) per update. For period=20, this is ~60 comparisons. For period=50, ~150 comparisons.
Warmup path:
During warmup (bars < period), only buffer fills occur — O(1) per bar.
Batch Mode (SIMD Analysis)
FRAMA is an IIR filter with sliding window min/max — not vectorizable across bars due to:
- Recursive EMA state dependency
- O(N) range scans that don't benefit from SIMD without monotonic deque optimization
| Optimization | Potential Benefit |
|---|---|
| Monotonic deque | O(1) amortized min/max (not implemented) |
| FMA instructions | ~2 cycle savings in final update |
Quality Metrics
| Metric | Score | Notes |
|---|---|---|
| Accuracy | 8/10 | Matches PineScript reference |
| Timeliness | 8/10 | Adapts to trends quickly |
| Overshoot | 5/10 | Can overshoot on sharp reversals |
| Smoothness | 7/10 | Smoother than EMA in noise |
Validation
FRAMA is not implemented in the common TA libraries used by QuanTAlib. Validation uses a direct reference implementation that mirrors the PineScript logic.
| Library | Status | Notes |
|---|---|---|
| TA-Lib | N/A | Not implemented |
| Skender | N/A | Not implemented |
| Tulip | N/A | Not implemented |
| Ooples | N/A | Not implemented |
| PineScript | ✅ | Matches lib/trends_IIR/frama/frama.pine |
C# Implementation Considerations
State Management
FRAMA uses a compact State struct with dual RingBuffer tracking:
[StructLayout(LayoutKind.Sequential)]
private struct State
{
public double Frama;
public double LastHigh;
public double LastLow;
public int Bars;
public bool HasValue;
}
Bar correction requires coordinated rollback of state and both ring buffers:
if (isNew) { _p_state = _state; _highs.Snapshot(); _lows.Snapshot(); }
else { _state = _p_state; _highs.Restore(); _lows.Restore(); }
Dual RingBuffer Architecture
FRAMA maintains separate High and Low buffers for fractal dimension calculation:
private readonly RingBuffer _highs;
private readonly RingBuffer _lows;
The GetMax and GetMin helper methods scan these buffers for range calculations, supporting both recent-half and full-window lookups via startOffset parameter.
Precomputed Constants
Constructor enforces even period and precalculates half-period:
int pe = (period % 2 == 0) ? period : period + 1;
_periodEven = pe;
_half = pe / 2;
Alpha bounds are compile-time constants:
private const double AlphaFloor = 0.01;
private const double AlphaCeil = 1.0;
private const double Log2 = 0.693147180559945309417232121458176568;
FMA Usage
The final EMA update uses FusedMultiplyAdd:
double result = Math.FusedMultiplyAdd(prev, 1.0 - alpha, alpha * price);
TBar Input Support
FRAMA accepts TBar input for proper High/Low access, with TValue fallback:
public TValue Update(TValue input, bool isNew = true)
{
return Update(new TBar(input.Time, input.Value, input.Value,
input.Value, input.Value, 0), isNew);
}
Memory Layout
| Field | Type | Size | Purpose |
|---|---|---|---|
_periodEven |
int | 4B | Even-adjusted period |
_half |
int | 4B | Half period for ranges |
_highs |
RingBuffer | ~8B+period×8B | High values buffer |
_lows |
RingBuffer | ~8B+period×8B | Low values buffer |
_state |
State | ~32B | Current calculation state |
_p_state |
State | ~32B | Previous state for rollback |
| Total | ~88B + 2×period×8B | Per indicator instance |
Range Scan Implementation
The GetMax/GetMin methods perform O(N) linear scans with modular indexing:
int idx = start + offset + i;
if (idx >= capacity) idx -= capacity;
This approach is simple and cache-friendly for typical periods (10-50). Monotonic deque optimization would reduce to O(1) amortized but adds complexity.
Common Pitfalls
- Period parity: The algorithm requires even
N. Odd values are rounded up. - Warmup: Outputs are
NaNuntilNbars are available. - Range source: FRAMA uses High and Low ranges. Feeding Close-only data collapses the ranges.
- Bar correction: Use
isNew=falsefor corrections so the last bar is recomputed safely.