mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-24 13:38:05 +00:00
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:
co-authored by
Claude Opus 4.5
aider
Warp
parent
5bcdf8d614
commit
86fe32a682
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user