Files
QuanTAlib/lib/trends_IIR/frama/Frama.md
T

7.5 KiB
Raw Blame History

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 pe bars 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:

  1. Compute ranges over the first half, second half, and full window.
  2. Convert range ratios to a dimension estimate.
  3. Convert dimension to a dynamic alpha.
  4. 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:

  1. HL2 price: (high + low) * 0.5 — 1 ADD + 1 MUL
  2. Range scans (3 windows): min/max over N, N/2, N/2 — 3×N CMP (60 for period=20)
  3. Range normalization: 3 DIV operations
  4. Fractal dimension: (ln(N1+N2) - ln(N3)) / ln(2) — 2 LOG + 1 ADD + 1 SUB + 1 DIV
  5. Alpha calculation: exp(-4.6 * (D - 1)) — 1 EXP + 1 MUL + 1 SUB
  6. 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:

  1. Recursive EMA state dependency
  2. 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

  1. Period parity: The algorithm requires even N. Odd values are rounded up.
  2. Warmup: Outputs are NaN until N bars are available.
  3. Range source: FRAMA uses High and Low ranges. Feeding Close-only data collapses the ranges.
  4. Bar correction: Use isNew=false for corrections so the last bar is recomputed safely.