SIMD Refactor: Merge simd-dev into dev (#55)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4) <aider@aider.chat>
Co-authored-by: Warp <agent@warp.dev>
This commit is contained in:
Miha Kralj
2026-01-18 19:02:03 -08:00
committed by GitHub
co-authored by Claude Opus 4.5 aider Warp
parent 5bcdf8d614
commit 86fe32a682
1750 changed files with 198235 additions and 80539 deletions
@@ -0,0 +1,112 @@
using TradingPlatform.BusinessLayer;
namespace QuanTAlib.Tests;
public class FramaIndicatorTests
{
[Fact]
public void FramaIndicator_Constructor_SetsDefaults()
{
var indicator = new FramaIndicator();
Assert.Equal(16, indicator.Period);
Assert.True(indicator.ShowColdValues);
Assert.Equal("FRAMA - Ehlers Fractal Adaptive Moving Average", indicator.Name);
Assert.False(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void FramaIndicator_MinHistoryDepths_ReturnsZero()
{
var indicator = new FramaIndicator { Period = 20 };
Assert.Equal(0, FramaIndicator.MinHistoryDepths);
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
[Fact]
public void FramaIndicator_ShortName_IncludesPeriod()
{
var indicator = new FramaIndicator { Period = 21 };
Assert.Contains("FRAMA", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("21", indicator.ShortName, StringComparison.Ordinal);
}
[Fact]
public void FramaIndicator_SourceCodeLink_IsValid()
{
var indicator = new FramaIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
Assert.Contains("Frama.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
}
[Fact]
public void FramaIndicator_Initialize_CreatesLineSeries()
{
var indicator = new FramaIndicator { Period = 10 };
indicator.Initialize();
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void FramaIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new FramaIndicator { Period = 4 };
indicator.Initialize();
var now = DateTime.UtcNow;
int warmup = indicator.Period % 2 == 0 ? indicator.Period : indicator.Period + 1;
for (int i = 0; i < warmup; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 102);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
Assert.Equal(warmup, indicator.LinesSeries[0].Count);
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
}
[Fact]
public void FramaIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new FramaIndicator { Period = 4 };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
[Fact]
public void FramaIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
{
var indicator = new FramaIndicator { Period = 4 };
indicator.Initialize();
var now = DateTime.UtcNow;
int warmup = indicator.Period % 2 == 0 ? indicator.Period : indicator.Period + 1;
for (int i = 0; i < warmup; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 102);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double firstValue = indicator.LinesSeries[0].GetValue(0);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
double secondValue = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(firstValue));
Assert.True(double.IsFinite(secondValue));
}
}
+56
View File
@@ -0,0 +1,56 @@
using System.Drawing;
using System.Runtime.CompilerServices;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
[SkipLocalsInit]
public class FramaIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Period (even enforced)", sortIndex: 1, 2, 1000, 1, 0)]
public int Period { get; set; } = 16;
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private Frama ma = null!;
protected LineSeries Series;
public static int MinHistoryDepths => 0;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => $"FRAMA {Period}";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/trends_IIR/frama/Frama.Quantower.cs";
public FramaIndicator()
{
OnBackGround = true;
SeparateWindow = false;
Name = "FRAMA - Ehlers Fractal Adaptive Moving Average";
Description = "Fractal Adaptive Moving Average using High/Low ranges and HL2 smoothing.";
Series = new LineSeries(name: $"FRAMA {Period}", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
AddLineSeries(Series);
}
protected override void OnInit()
{
ma = new Frama(Period);
base.OnInit();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnUpdate(UpdateArgs args)
{
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
var bar = new TBar(
item.TimeLeft.Ticks,
item[PriceType.Open],
item[PriceType.High],
item[PriceType.Low],
item[PriceType.Close],
item[PriceType.Volume]);
TValue result = ma.Update(bar, isNew: args.IsNewBar());
Series.SetValue(result.Value, ma.IsHot, ShowColdValues);
}
}
+160
View File
@@ -0,0 +1,160 @@
using System;
using System.Collections.Generic;
namespace QuanTAlib.Tests;
public class FramaTests
{
[Fact]
public void Frama_Constructor_ValidatesInput()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new Frama(1));
Assert.Throws<ArgumentOutOfRangeException>(() => new Frama(0));
Assert.Throws<ArgumentOutOfRangeException>(() => new Frama(-5));
}
[Fact]
public void Frama_BasicCalculation_ReturnsFinite()
{
var frama = new Frama(16);
var series = BuildSeries(40, seed: 42);
TValue result = default;
for (int i = 0; i < series.Count; i++)
{
result = frama.Update(series[i], isNew: true);
}
Assert.True(double.IsFinite(result.Value));
Assert.True(frama.IsHot);
}
[Fact]
public void Frama_IsNewFalse_RestoresState()
{
var frama = new Frama(16);
var series = BuildSeries(20, seed: 7);
TBar lastBar = default;
for (int i = 0; i < 10; i++)
{
lastBar = series[i];
frama.Update(lastBar, isNew: true);
}
double original = frama.Last.Value;
var corrected = new TBar(lastBar.Time, lastBar.Open, lastBar.High * 1.05, lastBar.Low * 0.95, lastBar.Close, lastBar.Volume);
frama.Update(corrected, isNew: false);
frama.Update(lastBar, isNew: false);
Assert.Equal(original, frama.Last.Value, precision: 10);
}
[Fact]
public void Frama_NaNFirstBar_RecoversOnValidInput()
{
var frama = new Frama(10);
int warmup = frama.WarmupPeriod;
var nanBar = new TBar(DateTime.UtcNow.Ticks, 1, double.NaN, 1, 1, 0);
TValue first = frama.Update(nanBar, isNew: true);
Assert.True(double.IsNaN(first.Value));
DateTime start = DateTime.UtcNow.AddMinutes(1);
TValue next = default;
for (int i = 0; i < warmup; i++)
{
var valid = new TBar(start.AddMinutes(i).Ticks, 100, 110, 90, 105, 1000);
next = frama.Update(valid, isNew: true);
}
Assert.True(double.IsFinite(next.Value));
Assert.True(frama.IsHot);
}
[Fact]
public void Frama_BatchMatchesStreaming()
{
int period = 20;
var series = BuildSeries(80, seed: 11);
TSeries batch = FramaBatch(series, period);
var frama = new Frama(period);
var streamValues = new List<double>(series.Count);
for (int i = 0; i < series.Count; i++)
{
streamValues.Add(frama.Update(series[i]).Value);
}
for (int i = 0; i < series.Count; i++)
{
Assert.Equal(batch[i].Value, streamValues[i], precision: 10);
}
}
[Fact]
public void Frama_SpanMatchesBatch()
{
int period = 18;
var series = BuildSeries(60, seed: 21);
double[] output = new double[series.Count];
Frama.Calculate(series.High.Values, series.Low.Values, period, output);
TSeries batch = FramaBatch(series, period);
for (int i = 0; i < series.Count; i++)
{
Assert.Equal(batch[i].Value, output[i], precision: 10);
}
}
[Fact]
public void Frama_Eventing_WorksWithTSeries()
{
int period = 12;
var source = new TSeries();
var frama = new Frama(source, period);
int count = 0;
frama.Pub += (object? sender, in TValueEventArgs args) => count++;
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 31);
for (int i = 0; i < 25; i++)
{
var bar = gbm.Next(isNew: true);
source.Add(bar.Time, bar.Close);
}
Assert.Equal(25, count);
}
[Fact]
public void Frama_WarmupPeriod_TransitionsIsHot()
{
var frama = new Frama(15);
int warmup = frama.WarmupPeriod;
var series = BuildSeries(warmup, seed: 100);
for (int i = 0; i < warmup - 1; i++)
{
frama.Update(series[i], isNew: true);
Assert.False(frama.IsHot);
}
frama.Update(series[warmup - 1], isNew: true);
Assert.True(frama.IsHot);
}
private static TBarSeries BuildSeries(int count, int seed)
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: seed);
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
}
private static TSeries FramaBatch(TBarSeries series, int period)
{
return Frama.Batch(series, period);
}
}
@@ -0,0 +1,155 @@
using System;
namespace QuanTAlib.Tests;
public class FramaValidationTests
{
[Fact]
public void Frama_Streaming_MatchesReference()
{
int period = 16;
TBarSeries series = BuildSeries(200, seed: 5);
double[] reference = new double[series.Count];
ReferenceFrama(series.High.Values, series.Low.Values, period, reference);
var frama = new Frama(period);
for (int i = 0; i < series.Count; i++)
{
double actual = frama.Update(series[i], isNew: true).Value;
Assert.Equal(reference[i], actual, precision: 10);
}
}
[Fact]
public void Frama_Batch_MatchesReference()
{
int period = 20;
TBarSeries series = BuildSeries(180, seed: 7);
double[] reference = new double[series.Count];
ReferenceFrama(series.High.Values, series.Low.Values, period, reference);
TSeries batch = Frama.Batch(series, period);
for (int i = 0; i < series.Count; i++)
{
Assert.Equal(reference[i], batch[i].Value, precision: 10);
}
}
[Fact]
public void Frama_Span_MatchesReference()
{
int period = 24;
TBarSeries series = BuildSeries(160, seed: 11);
double[] output = new double[series.Count];
double[] reference = new double[series.Count];
ReferenceFrama(series.High.Values, series.Low.Values, period, reference);
Frama.Calculate(series.High.Values, series.Low.Values, period, output);
for (int i = 0; i < series.Count; i++)
{
Assert.Equal(reference[i], output[i], precision: 10);
}
}
private static void ReferenceFrama(ReadOnlySpan<double> high, ReadOnlySpan<double> low, int period, Span<double> output)
{
int pe = (period % 2 == 0) ? period : period + 1;
int h = pe / 2;
double lastHigh = double.NaN;
double lastLow = double.NaN;
double fr = double.NaN;
bool hasValue = false;
for (int i = 0; i < high.Length; i++)
{
double highVal = high[i];
double lowVal = low[i];
if (!double.IsFinite(highVal) || !double.IsFinite(lowVal))
{
if (!double.IsFinite(lastHigh) || !double.IsFinite(lastLow))
{
output[i] = double.NaN;
continue;
}
highVal = lastHigh;
lowVal = lastLow;
}
lastHigh = highVal;
lastLow = lowVal;
if (i < pe - 1)
{
output[i] = double.NaN;
continue;
}
double maxRecent = double.MinValue;
double minRecent = double.MaxValue;
double maxPrev = double.MinValue;
double minPrev = double.MaxValue;
double maxFull = double.MinValue;
double minFull = double.MaxValue;
int startFull = i - pe + 1;
int startRecent = i - h + 1;
for (int j = startFull; j <= i; j++)
{
double hv = high[j];
double lv = low[j];
if (!double.IsFinite(hv) || !double.IsFinite(lv))
{
hv = lastHigh;
lv = lastLow;
}
if (hv > maxFull) maxFull = hv;
if (lv < minFull) minFull = lv;
if (j >= startRecent)
{
if (hv > maxRecent) maxRecent = hv;
if (lv < minRecent) minRecent = lv;
}
else
{
if (hv > maxPrev) maxPrev = hv;
if (lv < minPrev) minPrev = lv;
}
}
double n1 = (maxRecent - minRecent) / h;
double n2 = (maxPrev - minPrev) / h;
double n3 = (maxFull - minFull) / pe;
double alpha = 1.0;
if (n1 > 0.0 && n2 > 0.0 && n3 > 0.0)
{
double dimen = (Math.Log(n1 + n2) - Math.Log(n3)) / 0.693147180559945309417232121458176568;
alpha = Math.Exp(-4.6 * (dimen - 1.0));
if (alpha < 0.01) alpha = 0.01;
if (alpha > 1.0) alpha = 1.0;
}
double price = (highVal + lowVal) * 0.5;
double prev = hasValue && double.IsFinite(fr) ? fr : price;
fr = Math.FusedMultiplyAdd(prev, 1.0 - alpha, alpha * price);
hasValue = true;
output[i] = fr;
}
}
private static TBarSeries BuildSeries(int count, int seed)
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: seed);
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
}
}
+326
View File
@@ -0,0 +1,326 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// FRAMA: Ehlers Fractal Adaptive Moving Average
/// </summary>
/// <remarks>
/// Classic Traders' Tips FRAMA:
/// - Ranges are computed from High/Low (not from source).
/// - Smoothed price is HL2.
/// - alpha = exp(-4.6 * (D - 1)), clamped to [0.01, 1].
/// - Period forced to even, >= 2.
/// </remarks>
[SkipLocalsInit]
public sealed class Frama : ITValuePublisher
{
private const double AlphaFloor = 0.01;
private const double AlphaCeil = 1.0;
private const double Log2 = 0.693147180559945309417232121458176568;
private readonly int _periodEven;
private readonly int _half;
private readonly RingBuffer _highs;
private readonly RingBuffer _lows;
private readonly TValuePublishedHandler _handler;
[StructLayout(LayoutKind.Sequential)]
private struct State
{
public double Frama;
public double LastHigh;
public double LastLow;
public int Bars;
public bool HasValue;
}
private State _state;
private State _p_state;
public string Name { get; }
public int WarmupPeriod { get; }
public bool IsHot => _state.Bars >= _periodEven;
public event TValuePublishedHandler? Pub;
public TValue Last { get; private set; }
public Frama(int period)
{
ArgumentOutOfRangeException.ThrowIfLessThan(period, 2);
int pe = (period % 2 == 0) ? period : period + 1;
_periodEven = pe;
_half = pe / 2;
_highs = new RingBuffer(pe);
_lows = new RingBuffer(pe);
_handler = Handle;
Name = $"Frama({period})";
WarmupPeriod = pe;
Reset();
}
public Frama(ITValuePublisher source, int period) : this(period)
{
source.Pub += _handler;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset()
{
_state = default;
_p_state = default;
_highs.Clear();
_lows.Clear();
Last = default;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TBar input, bool isNew = true)
{
if (isNew)
{
_p_state = _state;
_highs.Snapshot();
_lows.Snapshot();
}
else
{
_state = _p_state;
_highs.Restore();
_lows.Restore();
}
double high = input.High;
double low = input.Low;
if (!double.IsFinite(high) || !double.IsFinite(low))
{
if (_state.Bars == 0)
{
Last = new TValue(input.Time, double.NaN);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
return Last;
}
high = _state.LastHigh;
low = _state.LastLow;
}
_state.LastHigh = high;
_state.LastLow = low;
_state.Bars++;
_highs.Add(high);
_lows.Add(low);
if (_state.Bars < _periodEven)
{
_state.Frama = double.NaN;
Last = new TValue(input.Time, double.NaN);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
return Last;
}
double price = (high + low) * 0.5;
double maxRecent = GetMax(_highs, _half);
double minRecent = GetMin(_lows, _half);
double maxFull = GetMax(_highs, _periodEven);
double minFull = GetMin(_lows, _periodEven);
double maxPrev = GetMax(_highs, _half, startOffset: 0);
double minPrev = GetMin(_lows, _half, startOffset: 0);
double n1 = (maxRecent - minRecent) / _half;
double n2 = (maxPrev - minPrev) / _half;
double n3 = (maxFull - minFull) / _periodEven;
double alpha = AlphaCeil;
if (n1 > 0.0 && n2 > 0.0 && n3 > 0.0)
{
double dimen = (Math.Log(n1 + n2) - Math.Log(n3)) / Log2;
alpha = Math.Exp(-4.6 * (dimen - 1.0));
if (alpha < AlphaFloor) alpha = AlphaFloor;
if (alpha > AlphaCeil) alpha = AlphaCeil;
}
double prev = _state.HasValue && double.IsFinite(_state.Frama) ? _state.Frama : price;
double result = Math.FusedMultiplyAdd(prev, 1.0 - alpha, alpha * price);
_state.Frama = result;
_state.HasValue = true;
Last = new TValue(input.Time, result);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
return Last;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
{
return Update(new TBar(input.Time, input.Value, input.Value, input.Value, input.Value, 0), isNew);
}
public TSeries Update(TBarSeries source)
{
if (source.Count == 0) return new TSeries([], []);
int len = source.Count;
var v = new double[len];
Calculate(source.High.Values, source.Low.Values, _periodEven, v);
var tList = new List<long>(len);
var times = source.Open.Times;
for (int i = 0; i < len; i++)
{
tList.Add(times[i]);
}
Reset();
for (int i = 0; i < len; i++)
{
Update(source[i], isNew: true);
}
return new TSeries(tList, [.. v]);
}
public TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
int len = source.Count;
var t = new List<long>(len);
var v = new List<double>(len);
CollectionsMarshal.SetCount(t, len);
CollectionsMarshal.SetCount(v, len);
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
source.Times.CopyTo(tSpan);
Reset();
for (int i = 0; i < len; i++)
{
TValue result = Update(source[i], isNew: true);
vSpan[i] = result.Value;
}
return new TSeries(t, v);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Handle(object? sender, in TValueEventArgs args) => Update(args.Value, args.IsNew);
public static void Calculate(ReadOnlySpan<double> high, ReadOnlySpan<double> low, int period, Span<double> output)
{
if (high.Length != low.Length || high.Length != output.Length)
throw new ArgumentException("Input spans must have the same length.", nameof(output));
ArgumentOutOfRangeException.ThrowIfLessThan(period, 2);
var frama = new Frama(period);
for (int i = 0; i < high.Length; i++)
{
var bar = new TBar(DateTime.MinValue, high[i], high[i], low[i], low[i], 0);
output[i] = frama.Update(bar, isNew: true).Value;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period)
{
if (source.Length != output.Length)
throw new ArgumentException("Source and output must have the same length.", nameof(output));
ArgumentOutOfRangeException.ThrowIfLessThan(period, 2);
var frama = new Frama(period);
for (int i = 0; i < source.Length; i++)
{
var bar = new TBar(DateTime.MinValue, source[i], source[i], source[i], source[i], 0);
output[i] = frama.Update(bar, isNew: true).Value;
}
}
public static TSeries Batch(TBarSeries source, int period)
{
if (source.Count == 0) return new TSeries([], []);
int len = source.Count;
var v = new double[len];
Calculate(source.High.Values, source.Low.Values, period, v);
var tList = new List<long>(len);
var times = source.Open.Times;
for (int i = 0; i < len; i++)
{
tList.Add(times[i]);
}
return new TSeries(tList, [.. v]);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double GetMax(RingBuffer buffer, int length, int startOffset = -1)
{
int count = buffer.Count;
if (count == 0 || length <= 0)
return double.NaN;
int capacity = buffer.Capacity;
int start = buffer.StartIndex;
ReadOnlySpan<double> data = buffer.InternalBuffer;
int offset = startOffset >= 0 ? startOffset : count - length;
double max = double.MinValue;
for (int i = 0; i < length; i++)
{
int idx = start + offset + i;
if (idx >= capacity)
idx -= capacity;
double v = data[idx];
if (v > max)
max = v;
}
return max;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double GetMin(RingBuffer buffer, int length, int startOffset = -1)
{
int count = buffer.Count;
if (count == 0 || length <= 0)
return double.NaN;
int capacity = buffer.Capacity;
int start = buffer.StartIndex;
ReadOnlySpan<double> data = buffer.InternalBuffer;
int offset = startOffset >= 0 ? startOffset : count - length;
double min = double.MaxValue;
for (int i = 0; i < length; i++)
{
int idx = start + offset + i;
if (idx >= capacity)
idx -= capacity;
double v = data[idx];
if (v < min)
min = v;
}
return min;
}
}
+210
View File
@@ -0,0 +1,210 @@
# FRAMA: Ehlers Fractal Adaptive Moving Average
> "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.
+60
View File
@@ -0,0 +1,60 @@
// The MIT License (MIT)
// © mihakralj
//@version=6
indicator("Ehlers Fractal Adaptive Moving Average (FRAMA)", "FRAMA", overlay=true)
// Ehlers FRAMA:
// - N1/N2/N3 computed from High/Low ranges (NOT from src).
// - Price being smoothed is HL2 ( (H+L)/2 ).
// - alpha = exp(-4.6*(D-1)), clamped to [0.01, 1].
// - Period forced to even, >= 2.
// References match the classic Traders' Tips FRAMA definition.
frama_strict(simple int period) =>
int p = math.max(2, period)
int pe = (p % 2 == 0) ? p : (p + 1)
int h = int(pe / 2)
// Price series per Ehlers FRAMA (commonly HL2)
float price = hl2
// Require enough history and non-NA ranges over the needed windows
bool ready =
bar_index >= pe - 1 and
not na(price) and
not na(ta.highest(high, pe)) and not na(ta.lowest(low, pe)) and
not na(ta.highest(high, h)) and not na(ta.lowest(low, h)) and
not na(ta.highest(high[h], h)) and not na(ta.lowest(low[h], h))
var float fr = na
if ready
// Ranges per Ehlers:
// N1: first half range / half
// N2: second half range / half (shifted by half)
// N3: full range / full
float n1 = (ta.highest(high, h) - ta.lowest(low, h)) / h
float n2 = (ta.highest(high[h], h) - ta.lowest(low[h], h)) / h
float n3 = (ta.highest(high, pe) - ta.lowest(low, pe)) / pe
float alpha = 1.0
if n1 > 0 and n2 > 0 and n3 > 0
float dimen = (math.log(n1 + n2) - math.log(n3)) / math.log(2.0)
alpha := math.exp(-4.6 * (dimen - 1.0))
alpha := math.max(0.01, math.min(1.0, alpha))
// Warm-start: first computed value seeds to price
float prev = nz(fr[1], price)
fr := alpha * price + (1.0 - alpha) * prev
else
fr := na
fr
// -------- Main --------
i_period = input.int(16, "Period (even enforced)", minval=2)
frama_value = frama_strict(i_period)
plot(frama_value, "FRAMA (strict)", color=color.yellow, linewidth=2)