Complete thin Dx-composition wrapper indicators with full test coverage: - PlusDi/MinusDi: Directional Indicator wrappers (DiPlus/DiMinus from Dx) - PlusDm/MinusDm: Directional Movement wrappers (DmPlus/DmMinus from Dx) - Individual validation tests per indicator directory (TALib, Skender, bounds) - Combined unit tests (DiDm.Tests.cs) and validation tests (DiDm.Validation.Tests.cs) - Quantower wrappers + tests for all 4 indicators - PineScript v6 implementations with compensated RMA - Normalized .md documentation for all indicators and categories - 182 tests passing, 0 failures
7.6 KiB
FRAMA: Ehlers Fractal Adaptive Moving Average
Markets do not move at one speed. FRAMA listens to the roughness and adjusts the filter.
| Property | Value |
|---|---|
| Category | Trend (IIR MA) |
| Inputs | OHLCV bar (TBar) |
| Parameters | period |
| Outputs | Single series (Frama) |
| Output range | Tracks input |
| Warmup | pe bars |
| PineScript | frama.pine |
| Signature | frama_signature |
- 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.
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.