mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-25 22:08:05 +00:00
Add TRAMA implementation and comprehensive tests
- Implemented the TRAMA (Trend Regularity Adaptive Moving Average) class with adaptive EMA logic. - Added unit tests for TRAMA functionality, including constructor validation, basic calculations, state management, and robustness checks. - Created validation tests to ensure consistency across different modes of operation (streaming, batch, and static calculations). - Enhanced documentation for TRAMA, including performance profiles and quality metrics. - Updated workspace configuration by removing unnecessary folder references.
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class AhrensIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new AhrensIndicator();
|
||||
|
||||
Assert.Equal(9, indicator.Period);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("AHRENS - Ahrens Moving Average", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new AhrensIndicator { Period = 20 };
|
||||
|
||||
Assert.Equal(0, AhrensIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShortName_IncludesPeriodAndSource()
|
||||
{
|
||||
var indicator = new AhrensIndicator { Period = 15 };
|
||||
|
||||
Assert.Contains("AHRENS", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("15", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialize_CreatesLineSeries()
|
||||
{
|
||||
var indicator = new AhrensIndicator { Period = 9 };
|
||||
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new AhrensIndicator { Period = 4 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new AhrensIndicator { 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 ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new AhrensIndicator { Period = 4 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 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));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentSourceTypes_Work()
|
||||
{
|
||||
var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 };
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var indicator = new AhrensIndicator { Period = 3, Source = source };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)),
|
||||
$"Source {source} should produce finite value");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Period_CanBeChanged()
|
||||
{
|
||||
var indicator = new AhrensIndicator { Period = 5 };
|
||||
Assert.Equal(5, indicator.Period);
|
||||
indicator.Period = 20;
|
||||
Assert.Equal(20, indicator.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new AhrensIndicator();
|
||||
Assert.Contains("Ahrens.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public class AhrensIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 1000, 1, 0)]
|
||||
public int Period { get; set; } = 9;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Ahrens ma = null!;
|
||||
protected LineSeries Series;
|
||||
protected string SourceName = null!;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"AHRENS {Period}:{SourceName}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/trends_IIR/ahrens/Ahrens.Quantower.cs";
|
||||
|
||||
public AhrensIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
SourceName = Source.ToString();
|
||||
Name = "AHRENS - Ahrens Moving Average";
|
||||
Description = "Self-dampening IIR filter using a lagged output buffer for inherent smoothing.";
|
||||
Series = new LineSeries(name: $"AHRENS {Period}", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(Series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
ma = new Ahrens(Period);
|
||||
SourceName = Source.ToString();
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
|
||||
TValue result = ma.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), isNew: args.IsNewBar());
|
||||
Series.SetValue(result.Value, ma.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class AhrensTests
|
||||
{
|
||||
private static TSeries MakeSeries(int count = 500)
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100, seed: 42);
|
||||
var series = new TSeries();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
series.Add(gbm.Next());
|
||||
}
|
||||
return series;
|
||||
}
|
||||
|
||||
// ── A) Constructor validation ──
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Period0_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new Ahrens(period: 0));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativePeriod_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new Ahrens(period: -1));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Period1_Valid()
|
||||
{
|
||||
var ind = new Ahrens(period: 1);
|
||||
Assert.Equal("Ahrens(1)", ind.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultPeriod_Is9()
|
||||
{
|
||||
var ind = new Ahrens();
|
||||
Assert.Equal("Ahrens(9)", ind.Name);
|
||||
Assert.Equal(9, ind.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SetsPeriodName()
|
||||
{
|
||||
var ind = new Ahrens(period: 20);
|
||||
Assert.Equal("Ahrens(20)", ind.Name);
|
||||
Assert.Equal(20, ind.WarmupPeriod);
|
||||
}
|
||||
|
||||
// ── B) Basic calculation ──
|
||||
|
||||
[Fact]
|
||||
public void Update_ReturnsTValue()
|
||||
{
|
||||
var ind = new Ahrens(9);
|
||||
TValue result = ind.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_LastIsAccessible()
|
||||
{
|
||||
var ind = new Ahrens(9);
|
||||
ind.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.True(double.IsFinite(ind.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_FirstBar_SeedsWithSource()
|
||||
{
|
||||
var ind = new Ahrens(9);
|
||||
TValue result = ind.Update(new TValue(DateTime.UtcNow, 50.0));
|
||||
// First bar: prev=source, lagged=source (empty buffer), midpoint=source
|
||||
// result = source + (source - source) / period = source
|
||||
Assert.Equal(50.0, result.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ConstantInput_ConvergesToConstant()
|
||||
{
|
||||
var ind = new Ahrens(9);
|
||||
double constant = 42.0;
|
||||
TValue result = default;
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
result = ind.Update(new TValue(DateTime.UtcNow, constant));
|
||||
}
|
||||
|
||||
Assert.Equal(constant, result.Value, 6);
|
||||
}
|
||||
|
||||
// ── C) State + bar correction ──
|
||||
|
||||
[Fact]
|
||||
public void IsNew_True_AdvancesState()
|
||||
{
|
||||
var ind = new Ahrens(9);
|
||||
TSeries src = MakeSeries(20);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
ind.Update(new TValue(DateTime.UtcNow, src.Values[i]), isNew: true);
|
||||
}
|
||||
|
||||
Assert.True(ind.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNew_False_RewritesSameBar()
|
||||
{
|
||||
var ind = new Ahrens(9);
|
||||
TSeries src = MakeSeries(15);
|
||||
for (int i = 0; i < 14; i++)
|
||||
{
|
||||
ind.Update(new TValue(DateTime.UtcNow, src.Values[i]));
|
||||
}
|
||||
|
||||
TValue first = ind.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
TValue second = ind.Update(new TValue(DateTime.UtcNow, 100.0), isNew: false);
|
||||
Assert.Equal(first.Value, second.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BarCorrection_Idempotent()
|
||||
{
|
||||
var ind = new Ahrens(9);
|
||||
TSeries src = MakeSeries(20);
|
||||
for (int i = 0; i < 19; i++)
|
||||
{
|
||||
ind.Update(new TValue(DateTime.UtcNow, src.Values[i]));
|
||||
}
|
||||
|
||||
TValue first = ind.Update(new TValue(DateTime.UtcNow, 55.0));
|
||||
_ = ind.Update(new TValue(DateTime.UtcNow, 60.0), isNew: false);
|
||||
_ = ind.Update(new TValue(DateTime.UtcNow, 65.0), isNew: false);
|
||||
TValue last = ind.Update(new TValue(DateTime.UtcNow, 55.0), isNew: false);
|
||||
Assert.Equal(first.Value, last.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrection_Restores()
|
||||
{
|
||||
var ind = new Ahrens(9);
|
||||
TSeries src = MakeSeries(30);
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
ind.Update(new TValue(DateTime.UtcNow, src.Values[i]));
|
||||
}
|
||||
|
||||
_ = ind.Update(new TValue(DateTime.UtcNow, 999.0));
|
||||
_ = ind.Update(new TValue(DateTime.UtcNow, src.Values[25]), isNew: false);
|
||||
Assert.True(double.IsFinite(ind.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var ind = new Ahrens(9);
|
||||
TSeries src = MakeSeries(20);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
ind.Update(new TValue(DateTime.UtcNow, src.Values[i]));
|
||||
}
|
||||
|
||||
Assert.True(ind.IsHot);
|
||||
ind.Reset();
|
||||
Assert.False(ind.IsHot);
|
||||
|
||||
TValue result = ind.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.Equal(100.0, result.Value, 10);
|
||||
}
|
||||
|
||||
// ── D) Warmup / convergence ──
|
||||
|
||||
[Fact]
|
||||
public void IsHot_FlipsAtPeriod()
|
||||
{
|
||||
var ind = new Ahrens(5);
|
||||
TSeries src = MakeSeries(10);
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
ind.Update(new TValue(DateTime.UtcNow, src.Values[i]));
|
||||
Assert.False(ind.IsHot);
|
||||
}
|
||||
|
||||
ind.Update(new TValue(DateTime.UtcNow, src.Values[4]));
|
||||
Assert.True(ind.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_MatchesPeriod()
|
||||
{
|
||||
var ind = new Ahrens(15);
|
||||
Assert.Equal(15, ind.WarmupPeriod);
|
||||
}
|
||||
|
||||
// ── E) Robustness ──
|
||||
|
||||
[Fact]
|
||||
public void NaN_UsesLastValidValue()
|
||||
{
|
||||
var ind = new Ahrens(9);
|
||||
ind.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
ind.Update(new TValue(DateTime.UtcNow, 110.0));
|
||||
|
||||
ind.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
Assert.True(double.IsFinite(ind.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_UsesLastValidValue()
|
||||
{
|
||||
var ind = new Ahrens(9);
|
||||
ind.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
ind.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(ind.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllNaN_ReturnsNaN()
|
||||
{
|
||||
var ind = new Ahrens(9);
|
||||
TValue result = ind.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
Assert.True(double.IsNaN(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchNaN_Safe()
|
||||
{
|
||||
double[] src = [1, 2, double.NaN, 4, 5];
|
||||
double[] output = new double[5];
|
||||
Ahrens.Batch(src, output, period: 3);
|
||||
Assert.True(double.IsFinite(output[0]));
|
||||
Assert.True(double.IsFinite(output[4]));
|
||||
}
|
||||
|
||||
// ── F) Consistency (4 API modes) ──
|
||||
|
||||
[Fact]
|
||||
public void AllModes_Match()
|
||||
{
|
||||
TSeries src = MakeSeries(200);
|
||||
int period = 9;
|
||||
|
||||
// Mode 1: Streaming
|
||||
var streaming = new Ahrens(period);
|
||||
double[] streamVals = new double[src.Count];
|
||||
for (int i = 0; i < src.Count; i++)
|
||||
{
|
||||
streamVals[i] = streaming.Update(new TValue(DateTime.UtcNow, src.Values[i])).Value;
|
||||
}
|
||||
|
||||
// Mode 2: Batch TSeries
|
||||
TSeries batch = Ahrens.Batch(src, period);
|
||||
|
||||
// Mode 3: Span
|
||||
double[] spanOut = new double[src.Count];
|
||||
Ahrens.Batch(src.Values, spanOut, period);
|
||||
|
||||
// Mode 4: Event-based
|
||||
var pub = new TSeries();
|
||||
var listener = new Ahrens(pub, period);
|
||||
double[] eventVals = new double[src.Count];
|
||||
for (int i = 0; i < src.Count; i++)
|
||||
{
|
||||
pub.Add(new TValue(DateTime.UtcNow, src.Values[i]));
|
||||
eventVals[i] = listener.Last.Value;
|
||||
}
|
||||
|
||||
// Compare after warmup
|
||||
for (int i = period; i < src.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamVals[i], batch.Values[i], 10);
|
||||
Assert.Equal(streamVals[i], spanOut[i], 10);
|
||||
Assert.Equal(streamVals[i], eventVals[i], 10);
|
||||
}
|
||||
}
|
||||
|
||||
// ── G) Span API tests ──
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_LengthMismatch_Throws()
|
||||
{
|
||||
double[] src = [1, 2, 3];
|
||||
double[] output = new double[2];
|
||||
var ex = Assert.Throws<ArgumentException>(() => Ahrens.Batch(src, output, period: 3));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_InvalidPeriod_Throws()
|
||||
{
|
||||
double[] src = [1, 2, 3];
|
||||
double[] output = new double[3];
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => Ahrens.Batch(src, output, period: 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_EmptySource_NoOp()
|
||||
{
|
||||
Ahrens.Batch(ReadOnlySpan<double>.Empty, Span<double>.Empty, period: 9);
|
||||
Assert.True(true); // no-throw is the assertion
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_MatchesTSeries()
|
||||
{
|
||||
TSeries src = MakeSeries(100);
|
||||
TSeries batchResult = Ahrens.Batch(src, 9);
|
||||
double[] spanOut = new double[src.Count];
|
||||
Ahrens.Batch(src.Values, spanOut, 9);
|
||||
|
||||
for (int i = 9; i < src.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult.Values[i], spanOut[i], 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_LargeData_NoStackOverflow()
|
||||
{
|
||||
int size = 5000;
|
||||
double[] src = new double[size];
|
||||
double[] output = new double[size];
|
||||
for (int i = 0; i < size; i++)
|
||||
{
|
||||
src[i] = 100.0 + (i * 0.01);
|
||||
}
|
||||
|
||||
Ahrens.Batch(src, output, period: 500);
|
||||
Assert.True(double.IsFinite(output[size - 1]));
|
||||
}
|
||||
|
||||
// ── H) Chainability ──
|
||||
|
||||
[Fact]
|
||||
public void PubFires()
|
||||
{
|
||||
var ind = new Ahrens(9);
|
||||
int fires = 0;
|
||||
ind.Pub += (object? sender, in TValueEventArgs e) => fires++;
|
||||
ind.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.Equal(1, fires);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventChaining_Works()
|
||||
{
|
||||
var src = new TSeries();
|
||||
var ahrens1 = new Ahrens(src, 9);
|
||||
var ahrens2 = new Ahrens(ahrens1, 5);
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
src.Add(new TValue(DateTime.UtcNow, 100.0 + i));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(ahrens2.Last.Value));
|
||||
Assert.True(ahrens1.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_UnsubscribesPublisher()
|
||||
{
|
||||
var src = new TSeries();
|
||||
var ind = new Ahrens(src, 9);
|
||||
src.Add(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.True(double.IsFinite(ind.Last.Value));
|
||||
|
||||
ind.Dispose();
|
||||
double before = ind.Last.Value;
|
||||
src.Add(new TValue(DateTime.UtcNow, 200.0));
|
||||
Assert.Equal(before, ind.Last.Value, 10);
|
||||
}
|
||||
|
||||
// ── AHRENS-specific ──
|
||||
|
||||
[Fact]
|
||||
public void Period1_EqualsSource()
|
||||
{
|
||||
var ind = new Ahrens(period: 1);
|
||||
TSeries src = MakeSeries(50);
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
TValue result = ind.Update(new TValue(DateTime.UtcNow, src.Values[i]));
|
||||
// period=1: lagged = buffer oldest = previous result, prev = previous result
|
||||
// midpoint = (prev + prev) / 2 = prev
|
||||
// result = prev + (source - prev) / 1 = source
|
||||
Assert.Equal(src.Values[i], result.Value, 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsBoth()
|
||||
{
|
||||
TSeries src = MakeSeries(100);
|
||||
(TSeries results, Ahrens indicator) = Ahrens.Calculate(src, 9);
|
||||
Assert.Equal(100, results.Count);
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelfDampening_SmoothsOutput()
|
||||
{
|
||||
var ind = new Ahrens(20);
|
||||
TSeries src = MakeSeries(500);
|
||||
double[] outputs = new double[500];
|
||||
for (int i = 0; i < 500; i++)
|
||||
{
|
||||
outputs[i] = ind.Update(new TValue(DateTime.UtcNow, src.Values[i])).Value;
|
||||
}
|
||||
|
||||
// Compare variance of last 100 values — output should be smoother
|
||||
double srcMean = 0, outMean = 0;
|
||||
for (int i = 400; i < 500; i++)
|
||||
{
|
||||
srcMean += src.Values[i];
|
||||
outMean += outputs[i];
|
||||
}
|
||||
|
||||
srcMean /= 100;
|
||||
outMean /= 100;
|
||||
|
||||
double srcVar = 0, outVar = 0;
|
||||
for (int i = 400; i < 500; i++)
|
||||
{
|
||||
double d1 = src.Values[i] - srcMean;
|
||||
srcVar += d1 * d1;
|
||||
double d2 = outputs[i] - outMean;
|
||||
outVar += d2 * d2;
|
||||
}
|
||||
|
||||
Assert.True(outVar < srcVar);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class AhrensValidationTests
|
||||
{
|
||||
private static TSeries MakeSeries(int count = 500)
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100, seed: 42);
|
||||
var series = new TSeries();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
series.Add(gbm.Next());
|
||||
}
|
||||
return series;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_And_Streaming_Match()
|
||||
{
|
||||
TSeries src = MakeSeries(1000);
|
||||
int period = 9;
|
||||
|
||||
TSeries batchResult = Ahrens.Batch(src, period);
|
||||
|
||||
var streaming = new Ahrens(period);
|
||||
for (int i = 0; i < src.Count; i++)
|
||||
{
|
||||
streaming.Update(new TValue(DateTime.UtcNow, src.Values[i]));
|
||||
}
|
||||
|
||||
for (int i = period; i < src.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult.Values[i], streaming.Last.Value is double _ ? batchResult.Values[i] : double.NaN, 10);
|
||||
}
|
||||
|
||||
// More direct: streaming last == batch last
|
||||
Assert.Equal(batchResult.Values[src.Count - 1], streaming.Last.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Span_And_Streaming_Match()
|
||||
{
|
||||
TSeries src = MakeSeries(1000);
|
||||
int period = 9;
|
||||
|
||||
double[] spanOut = new double[src.Count];
|
||||
Ahrens.Batch(src.Values, spanOut, period);
|
||||
|
||||
var streaming = new Ahrens(period);
|
||||
double[] streamVals = new double[src.Count];
|
||||
for (int i = 0; i < src.Count; i++)
|
||||
{
|
||||
streamVals[i] = streaming.Update(new TValue(DateTime.UtcNow, src.Values[i])).Value;
|
||||
}
|
||||
|
||||
for (int i = period; i < src.Count; i++)
|
||||
{
|
||||
Assert.Equal(spanOut[i], streamVals[i], 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1)]
|
||||
[InlineData(3)]
|
||||
[InlineData(9)]
|
||||
[InlineData(20)]
|
||||
[InlineData(50)]
|
||||
public void DifferentPeriods_AllFinite(int period)
|
||||
{
|
||||
TSeries src = MakeSeries(200);
|
||||
TSeries result = Ahrens.Batch(src, period);
|
||||
|
||||
for (int i = period; i < result.Count; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(result.Values[i]), $"Non-finite at index {i} for period {period}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constant_ConvergesToConstant()
|
||||
{
|
||||
int period = 9;
|
||||
double constant = 100.0;
|
||||
var ind = new Ahrens(period);
|
||||
for (int i = 0; i < 500; i++)
|
||||
{
|
||||
ind.Update(new TValue(DateTime.UtcNow, constant));
|
||||
}
|
||||
|
||||
Assert.Equal(constant, ind.Last.Value, 8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsHotIndicator()
|
||||
{
|
||||
TSeries src = MakeSeries(200);
|
||||
(TSeries results, Ahrens indicator) = Ahrens.Calculate(src, 9);
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.Equal(src.Count, results.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BarCorrection_Consistency()
|
||||
{
|
||||
TSeries src = MakeSeries(100);
|
||||
int period = 9;
|
||||
|
||||
// Run full series
|
||||
var ind1 = new Ahrens(period);
|
||||
for (int i = 0; i < src.Count; i++)
|
||||
{
|
||||
ind1.Update(new TValue(DateTime.UtcNow, src.Values[i]));
|
||||
}
|
||||
|
||||
double fullResult = ind1.Last.Value;
|
||||
|
||||
// Run with bar corrections at every bar
|
||||
var ind2 = new Ahrens(period);
|
||||
for (int i = 0; i < src.Count; i++)
|
||||
{
|
||||
// First update with wrong value
|
||||
ind2.Update(new TValue(DateTime.UtcNow, src.Values[i] + 10.0));
|
||||
// Correct it
|
||||
ind2.Update(new TValue(DateTime.UtcNow, src.Values[i]), isNew: false);
|
||||
// Then advance
|
||||
if (i < src.Count - 1)
|
||||
{
|
||||
// The next isNew=true will snapshot the corrected state
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Equal(fullResult, ind2.Last.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubsetStability()
|
||||
{
|
||||
TSeries src = MakeSeries(500);
|
||||
int period = 9;
|
||||
|
||||
// Run full 500 bars
|
||||
var full = new Ahrens(period);
|
||||
for (int i = 0; i < 500; i++)
|
||||
{
|
||||
full.Update(new TValue(DateTime.UtcNow, src.Values[i]));
|
||||
}
|
||||
|
||||
// Run only first 300 bars
|
||||
var partial = new Ahrens(period);
|
||||
for (int i = 0; i < 300; i++)
|
||||
{
|
||||
partial.Update(new TValue(DateTime.UtcNow, src.Values[i]));
|
||||
}
|
||||
|
||||
// Continue the partial from 300 to 500
|
||||
for (int i = 300; i < 500; i++)
|
||||
{
|
||||
partial.Update(new TValue(DateTime.UtcNow, src.Values[i]));
|
||||
}
|
||||
|
||||
Assert.Equal(full.Last.Value, partial.Last.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LargeDataset_NoOverflow()
|
||||
{
|
||||
TSeries src = MakeSeries(5000);
|
||||
int period = 50;
|
||||
|
||||
TSeries result = Ahrens.Batch(src, period);
|
||||
Assert.Equal(5000, result.Count);
|
||||
Assert.True(double.IsFinite(result.Values[result.Count - 1]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// AHRENS: Ahrens Moving Average
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A self-dampening IIR filter that uses a circular buffer of its own past
|
||||
/// output values. The correction term shrinks as current and lagged states
|
||||
/// converge, producing inherent smoothing without explicit decay constants.
|
||||
///
|
||||
/// Formula: AHRENS[t] = AHRENS[t-1] + (source - (AHRENS[t-1] + AHRENS[t-N]) / 2) / N
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Ahrens : AbstractBase
|
||||
{
|
||||
private const int MaxPeriod = 4000;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(int Bars, bool IsHot)
|
||||
{
|
||||
public double Prev;
|
||||
public static State New() => new() { Bars = 0, IsHot = false, Prev = double.NaN };
|
||||
}
|
||||
|
||||
private readonly int _period;
|
||||
private readonly double _invPeriod;
|
||||
private readonly RingBuffer _buffer; // stores past AHRENS output values
|
||||
|
||||
private State _state = State.New();
|
||||
private State _p_state = State.New();
|
||||
private double _lastValidValue = double.NaN;
|
||||
private double _p_lastValidValue = double.NaN;
|
||||
|
||||
private readonly ITValuePublisher? _publisher;
|
||||
private readonly TValuePublishedHandler? _listener;
|
||||
|
||||
public override bool IsHot => _state.IsHot;
|
||||
|
||||
public Ahrens(int period = 9)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(period, 1);
|
||||
|
||||
_period = Math.Min(period, MaxPeriod);
|
||||
_invPeriod = 1.0 / _period;
|
||||
_buffer = new RingBuffer(_period);
|
||||
|
||||
Name = $"Ahrens({period})";
|
||||
WarmupPeriod = _period;
|
||||
|
||||
Reset();
|
||||
}
|
||||
|
||||
public Ahrens(ITValuePublisher source, int period = 9) : this(period)
|
||||
{
|
||||
_publisher = source;
|
||||
_listener = Handle;
|
||||
source.Pub += _listener;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_p_state = _state;
|
||||
_p_lastValidValue = _lastValidValue;
|
||||
_buffer.Snapshot();
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = _p_state;
|
||||
_lastValidValue = _p_lastValidValue;
|
||||
_buffer.Restore();
|
||||
}
|
||||
|
||||
double val = input.Value;
|
||||
if (double.IsFinite(val))
|
||||
{
|
||||
_lastValidValue = val;
|
||||
}
|
||||
else
|
||||
{
|
||||
val = _lastValidValue;
|
||||
}
|
||||
|
||||
if (double.IsNaN(val))
|
||||
{
|
||||
Last = new TValue(input.Time, double.NaN);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
var s = _state;
|
||||
s.Bars++;
|
||||
|
||||
double result = Compute(val, ref s);
|
||||
|
||||
_state = s;
|
||||
Last = new TValue(input.Time, result);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
|
||||
public override 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);
|
||||
|
||||
_buffer.Snapshot();
|
||||
|
||||
State preBatchState = _state;
|
||||
double preBatchLastValid = _lastValidValue;
|
||||
State state = _state;
|
||||
double lastValid = _lastValidValue;
|
||||
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double val = source.Values[i];
|
||||
if (double.IsFinite(val))
|
||||
{
|
||||
lastValid = val;
|
||||
}
|
||||
else
|
||||
{
|
||||
val = lastValid;
|
||||
}
|
||||
|
||||
if (double.IsNaN(val))
|
||||
{
|
||||
vSpan[i] = double.NaN;
|
||||
continue;
|
||||
}
|
||||
|
||||
state.Bars++;
|
||||
vSpan[i] = Compute(val, ref state);
|
||||
}
|
||||
|
||||
_state = state;
|
||||
_lastValidValue = lastValid;
|
||||
_p_state = preBatchState;
|
||||
_p_lastValidValue = preBatchLastValid;
|
||||
}
|
||||
catch
|
||||
{
|
||||
_buffer.Restore();
|
||||
throw;
|
||||
}
|
||||
|
||||
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
foreach (double value in source)
|
||||
{
|
||||
Update(new TValue(DateTime.MinValue, value));
|
||||
}
|
||||
}
|
||||
|
||||
public static TSeries Batch(TSeries source, int period = 9)
|
||||
{
|
||||
var ahrens = new Ahrens(period);
|
||||
return ahrens.Update(source);
|
||||
}
|
||||
|
||||
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period = 9)
|
||||
{
|
||||
if (source.Length != output.Length)
|
||||
{
|
||||
throw new ArgumentException("Source and output must have the same length.", nameof(output));
|
||||
}
|
||||
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(period, 1);
|
||||
|
||||
if (source.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int window = Math.Min(period, MaxPeriod);
|
||||
double invPeriod = 1.0 / window;
|
||||
double lastValid = double.NaN;
|
||||
double prev = double.NaN;
|
||||
|
||||
Span<double> buffer = window <= 256
|
||||
? stackalloc double[window]
|
||||
: new double[window];
|
||||
|
||||
int head = 0;
|
||||
int count = 0; // tracks how many values written to buffer
|
||||
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
double val = source[i];
|
||||
if (double.IsFinite(val))
|
||||
{
|
||||
lastValid = val;
|
||||
}
|
||||
else
|
||||
{
|
||||
val = lastValid;
|
||||
}
|
||||
|
||||
if (double.IsNaN(val))
|
||||
{
|
||||
output[i] = double.NaN;
|
||||
continue;
|
||||
}
|
||||
|
||||
// First bar: seed with source value
|
||||
if (double.IsNaN(prev))
|
||||
{
|
||||
prev = val;
|
||||
}
|
||||
|
||||
// Get lagged value: oldest written result in buffer, or source if buffer empty
|
||||
// This matches streaming Compute where _buffer.Oldest returns first stored result
|
||||
double lagged;
|
||||
if (count > 0)
|
||||
{
|
||||
// oldest written index = (head - count + window) % window
|
||||
int oldestIdx = head - count;
|
||||
if (oldestIdx < 0)
|
||||
{
|
||||
oldestIdx += window;
|
||||
}
|
||||
lagged = buffer[oldestIdx];
|
||||
}
|
||||
else
|
||||
{
|
||||
lagged = val;
|
||||
}
|
||||
|
||||
// AHRENS formula: result = prev + (source - midpoint) / period
|
||||
// midpoint = (prev + lagged) * 0.5
|
||||
double midpoint = (prev + lagged) * 0.5;
|
||||
double result = Math.FusedMultiplyAdd(val - midpoint, invPeriod, prev);
|
||||
|
||||
// Store output in buffer and advance head
|
||||
buffer[head] = result;
|
||||
head++;
|
||||
if (head == window)
|
||||
{
|
||||
head = 0;
|
||||
}
|
||||
if (count < window)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
|
||||
prev = result;
|
||||
output[i] = result;
|
||||
}
|
||||
}
|
||||
|
||||
public static (TSeries Results, Ahrens Indicator) Calculate(TSeries source, int period = 9)
|
||||
{
|
||||
var indicator = new Ahrens(period);
|
||||
TSeries results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
_state = State.New();
|
||||
_p_state = _state;
|
||||
_lastValidValue = double.NaN;
|
||||
_p_lastValidValue = double.NaN;
|
||||
_buffer.Clear();
|
||||
Last = default;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && _publisher != null && _listener != null)
|
||||
{
|
||||
_publisher.Pub -= _listener;
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double Compute(double val, ref State s)
|
||||
{
|
||||
// First bar: seed prev with incoming value
|
||||
if (double.IsNaN(s.Prev))
|
||||
{
|
||||
s.Prev = val;
|
||||
}
|
||||
|
||||
// Get lagged AHRENS output from N bars ago
|
||||
double lagged;
|
||||
if (_buffer.Count > 0)
|
||||
{
|
||||
lagged = _buffer.Oldest;
|
||||
}
|
||||
else
|
||||
{
|
||||
lagged = val;
|
||||
}
|
||||
|
||||
// AHRENS formula: result = prev + (source - (prev + lagged) / 2) / N
|
||||
double midpoint = (s.Prev + lagged) * 0.5;
|
||||
double result = Math.FusedMultiplyAdd(val - midpoint, _invPeriod, s.Prev);
|
||||
|
||||
// Store output in buffer (buffer holds past AHRENS outputs)
|
||||
_buffer.Add(result);
|
||||
|
||||
s.Prev = result;
|
||||
|
||||
if (!s.IsHot && s.Bars >= _period)
|
||||
{
|
||||
s.IsHot = true;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user