Files
QuanTAlib/lib/trends_IIR/frama/Frama.md
T
2026-02-27 07:48:12 -08:00

228 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 |
### 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:
```csharp
[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:
```csharp
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:
```csharp
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:
```csharp
int pe = (period % 2 == 0) ? period : period + 1;
_periodEven = pe;
_half = pe / 2;
```
Alpha bounds are compile-time constants:
```csharp
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:
```csharp
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:
```csharp
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:
```csharp
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.