mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-19 02:58:05 +00:00
Refactor documentation links in numerics, oscillators, reversals, and statistics modules to use relative paths; update Bias class to handle division by zero more robustly; remove obsolete CUMMEAN Pine script; enhance trend indicators documentation; add Visual Studio Code workspace configuration.
This commit is contained in:
@@ -0,0 +1,362 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Quantower.Tests;
|
||||
|
||||
public class EacpIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void EacpIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new EacpIndicator();
|
||||
|
||||
Assert.Equal(8, indicator.MinPeriod);
|
||||
Assert.Equal(48, indicator.MaxPeriod);
|
||||
Assert.Equal(3, indicator.AvgLength);
|
||||
Assert.True(indicator.Enhance);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("EACP - Ehlers Autocorrelation Periodogram", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EacpIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new EacpIndicator();
|
||||
|
||||
Assert.Equal(0, EacpIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EacpIndicator_ShortName_IncludesPeriods()
|
||||
{
|
||||
var indicator = new EacpIndicator { MinPeriod = 10, MaxPeriod = 60 };
|
||||
|
||||
Assert.True(indicator.ShortName.Contains("EACP", StringComparison.Ordinal));
|
||||
Assert.True(indicator.ShortName.Contains("10", StringComparison.Ordinal));
|
||||
Assert.True(indicator.ShortName.Contains("60", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EacpIndicator_Initialize_CreatesInternalEacp()
|
||||
{
|
||||
var indicator = new EacpIndicator { MinPeriod = 8, MaxPeriod = 48 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist (Cycle + Power)
|
||||
Assert.Equal(2, indicator.LinesSeries.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EacpIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new EacpIndicator { MinPeriod = 8, MaxPeriod = 48 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process update
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
// Line series should have a value
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EacpIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new EacpIndicator { MinPeriod = 8, MaxPeriod = 48 };
|
||||
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 EacpIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new EacpIndicator { MinPeriod = 8, MaxPeriod = 48 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Should not throw an exception
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
|
||||
// Assert that the indicator still exists
|
||||
Assert.NotNull(indicator);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EacpIndicator_MultipleUpdates_ProducesCorrectSequence()
|
||||
{
|
||||
var indicator = new EacpIndicator { MinPeriod = 8, MaxPeriod = 48 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = { 100, 102, 105, 103, 107, 110, 108, 112, 115, 113 };
|
||||
|
||||
foreach (var close in closes)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now, close, close + 2, close - 2, close);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
now = now.AddMinutes(1);
|
||||
}
|
||||
|
||||
// All values should be finite
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(closes.Length - 1 - i)));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EacpIndicator_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 EacpIndicator { MinPeriod = 8, MaxPeriod = 48, 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 EacpIndicator_MinPeriod_CanBeChanged()
|
||||
{
|
||||
var indicator = new EacpIndicator { MinPeriod = 8 };
|
||||
|
||||
Assert.Equal(8, indicator.MinPeriod);
|
||||
|
||||
indicator.MinPeriod = 12;
|
||||
Assert.Equal(12, indicator.MinPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EacpIndicator_MaxPeriod_CanBeChanged()
|
||||
{
|
||||
var indicator = new EacpIndicator { MaxPeriod = 48 };
|
||||
|
||||
Assert.Equal(48, indicator.MaxPeriod);
|
||||
|
||||
indicator.MaxPeriod = 100;
|
||||
Assert.Equal(100, indicator.MaxPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EacpIndicator_AvgLength_CanBeChanged()
|
||||
{
|
||||
var indicator = new EacpIndicator { AvgLength = 3 };
|
||||
|
||||
Assert.Equal(3, indicator.AvgLength);
|
||||
|
||||
indicator.AvgLength = 10;
|
||||
Assert.Equal(10, indicator.AvgLength);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EacpIndicator_Enhance_CanBeChanged()
|
||||
{
|
||||
var indicator = new EacpIndicator { Enhance = true };
|
||||
|
||||
Assert.True(indicator.Enhance);
|
||||
|
||||
indicator.Enhance = false;
|
||||
Assert.False(indicator.Enhance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EacpIndicator_Source_CanBeChanged()
|
||||
{
|
||||
var indicator = new EacpIndicator { Source = SourceType.Close };
|
||||
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
|
||||
indicator.Source = SourceType.Open;
|
||||
Assert.Equal(SourceType.Open, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EacpIndicator_ShowColdValues_CanBeChanged()
|
||||
{
|
||||
var indicator = new EacpIndicator { ShowColdValues = true };
|
||||
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EacpIndicator_ShortName_UpdatesWhenPeriodsChange()
|
||||
{
|
||||
var indicator = new EacpIndicator { MinPeriod = 8, MaxPeriod = 48 };
|
||||
string initialName = indicator.ShortName;
|
||||
|
||||
Assert.True(initialName.Contains("8", StringComparison.Ordinal));
|
||||
Assert.True(initialName.Contains("48", StringComparison.Ordinal));
|
||||
|
||||
indicator.MinPeriod = 10;
|
||||
indicator.MaxPeriod = 60;
|
||||
string updatedName = indicator.ShortName;
|
||||
|
||||
Assert.True(updatedName.Contains("10", StringComparison.Ordinal));
|
||||
Assert.True(updatedName.Contains("60", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EacpIndicator_ProcessUpdate_IgnoresNonBarUpdates()
|
||||
{
|
||||
var indicator = new EacpIndicator { MinPeriod = 8, MaxPeriod = 48 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process historical bar first
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Process other update reasons - should not throw
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
|
||||
Assert.NotNull(indicator);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EacpIndicator_CycleSeries_HasCorrectProperties()
|
||||
{
|
||||
var indicator = new EacpIndicator { MinPeriod = 8, MaxPeriod = 48 };
|
||||
indicator.Initialize();
|
||||
|
||||
var lineSeries = indicator.LinesSeries[0];
|
||||
|
||||
Assert.Equal("Cycle", lineSeries.Name);
|
||||
Assert.Equal(2, lineSeries.Width);
|
||||
Assert.Equal(LineStyle.Solid, lineSeries.Style);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EacpIndicator_PowerSeries_HasCorrectProperties()
|
||||
{
|
||||
var indicator = new EacpIndicator { MinPeriod = 8, MaxPeriod = 48 };
|
||||
indicator.Initialize();
|
||||
|
||||
var powerSeries = indicator.LinesSeries[1];
|
||||
|
||||
Assert.Equal("Power", powerSeries.Name);
|
||||
Assert.Equal(1, powerSeries.Width);
|
||||
Assert.Equal(LineStyle.Dot, powerSeries.Style);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EacpIndicator_DifferentPeriodRanges_Work()
|
||||
{
|
||||
var periodRanges = new[] { (8, 48), (10, 60), (6, 30), (12, 100) };
|
||||
|
||||
foreach (var (minPeriod, maxPeriod) in periodRanges)
|
||||
{
|
||||
var indicator = new EacpIndicator { MinPeriod = minPeriod, MaxPeriod = maxPeriod };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// Add enough bars
|
||||
for (int i = 0; i < maxPeriod + 10; i++)
|
||||
{
|
||||
double close = 100 + (i % 10);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), close, close + 2, close - 2, close);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// Last value should be finite
|
||||
double cycleValue = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(cycleValue), $"Period range ({minPeriod},{maxPeriod}) should produce finite value");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EacpIndicator_SineWave_DetectsCycle()
|
||||
{
|
||||
var indicator = new EacpIndicator { MinPeriod = 8, MaxPeriod = 48 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
const int knownPeriod = 20;
|
||||
|
||||
// Generate sine wave pattern
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
double price = 100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / knownPeriod);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 1, price - 1, price);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// Cycle value should be in valid range
|
||||
double cycleValue = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.InRange(cycleValue, 8, 48);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EacpIndicator_PowerOutput_ScaledCorrectly()
|
||||
{
|
||||
var indicator = new EacpIndicator { MinPeriod = 8, MaxPeriod = 48 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double price = 100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 20.0);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 1, price - 1, price);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// Power is scaled by MaxPeriod
|
||||
double powerValue = indicator.LinesSeries[1].GetValue(0);
|
||||
Assert.True(double.IsFinite(powerValue));
|
||||
Assert.True(powerValue >= 0, "Power should be non-negative");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EacpIndicator_EnhanceMode_AffectsOutput()
|
||||
{
|
||||
var indicatorEnhanced = new EacpIndicator { MinPeriod = 8, MaxPeriod = 48, Enhance = true };
|
||||
var indicatorNormal = new EacpIndicator { MinPeriod = 8, MaxPeriod = 48, Enhance = false };
|
||||
indicatorEnhanced.Initialize();
|
||||
indicatorNormal.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Add same data to both
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double price = 100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 20.0);
|
||||
indicatorEnhanced.HistoricalData.AddBar(now.AddMinutes(i), price, price + 1, price - 1, price);
|
||||
indicatorNormal.HistoricalData.AddBar(now.AddMinutes(i), price, price + 1, price - 1, price);
|
||||
indicatorEnhanced.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicatorNormal.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// Both should produce finite values
|
||||
Assert.True(double.IsFinite(indicatorEnhanced.LinesSeries[0].GetValue(0)));
|
||||
Assert.True(double.IsFinite(indicatorNormal.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class EacpIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Min Period", sortIndex: 1, 3, 100, 1, 0)]
|
||||
public int MinPeriod { get; set; } = 8;
|
||||
|
||||
[InputParameter("Max Period", sortIndex: 2, 4, 500, 1, 0)]
|
||||
public int MaxPeriod { get; set; } = 48;
|
||||
|
||||
[InputParameter("Avg Length", sortIndex: 3, 0, 100, 1, 0)]
|
||||
public int AvgLength { get; set; } = 3;
|
||||
|
||||
[InputParameter("Enhance", sortIndex: 4)]
|
||||
public bool Enhance { get; set; } = true;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Eacp _eacp = null!;
|
||||
private readonly LineSeries _cycleSeries;
|
||||
private readonly LineSeries _powerSeries;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"EACP ({MinPeriod},{MaxPeriod})";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/cycles/eacp/Eacp.Quantower.cs";
|
||||
|
||||
public EacpIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "EACP - Ehlers Autocorrelation Periodogram";
|
||||
Description = "Ehlers' Autocorrelation Periodogram estimates the dominant cycle period using autocorrelation and spectral analysis";
|
||||
|
||||
_cycleSeries = new LineSeries(name: "Cycle", color: IndicatorExtensions.Oscillators, width: 2, style: LineStyle.Solid);
|
||||
_powerSeries = new LineSeries(name: "Power", color: Color.Orange, width: 1, style: LineStyle.Dot);
|
||||
AddLineSeries(_cycleSeries);
|
||||
AddLineSeries(_powerSeries);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_eacp = new Eacp(MinPeriod, MaxPeriod, AvgLength, Enhance);
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
if (args.Reason != UpdateReason.NewBar && args.Reason != UpdateReason.HistoricalBar)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _priceSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var input = new TValue(time, value);
|
||||
TValue result = _eacp.Update(input, args.IsNewBar());
|
||||
|
||||
_cycleSeries.SetValue(result.Value, _eacp.IsHot, ShowColdValues);
|
||||
_powerSeries.SetValue(_eacp.NormalizedPower * MaxPeriod, _eacp.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,524 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class EacpTests
|
||||
{
|
||||
private const double Tolerance = 1e-9;
|
||||
|
||||
#region Constructor Tests
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultParameters_SetsProperties()
|
||||
{
|
||||
var eacp = new Eacp();
|
||||
|
||||
Assert.Equal("Eacp(8,48)", eacp.Name);
|
||||
Assert.False(eacp.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_CustomParameters_SetsProperties()
|
||||
{
|
||||
var eacp = new Eacp(minPeriod: 10, maxPeriod: 60, avgLength: 5, enhance: false);
|
||||
|
||||
Assert.Equal("Eacp(10,60)", eacp.Name);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(2)]
|
||||
[InlineData(0)]
|
||||
[InlineData(-1)]
|
||||
public void Constructor_InvalidMinPeriod_ThrowsArgumentOutOfRange(int minPeriod)
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new Eacp(minPeriod, 48));
|
||||
Assert.Equal("minPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(8, 8)]
|
||||
[InlineData(8, 5)]
|
||||
[InlineData(10, 10)]
|
||||
public void Constructor_MaxPeriodNotGreaterThanMin_ThrowsArgumentOutOfRange(int minPeriod, int maxPeriod)
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new Eacp(minPeriod, maxPeriod));
|
||||
Assert.Equal("maxPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativeAvgLength_ThrowsArgumentOutOfRange()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new Eacp(8, 48, avgLength: -1));
|
||||
Assert.Equal("avgLength", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithNullSource_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => new Eacp(null!, 8, 48));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithValidSource_Subscribes()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var eacp = new Eacp(source, 8, 48);
|
||||
|
||||
source.Add(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.NotEqual(default, eacp.Last);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Basic Calculation Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_ReturnsValidTValue()
|
||||
{
|
||||
var eacp = new Eacp(8, 48);
|
||||
var result = eacp.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_AfterWarmup_IsHotTrue()
|
||||
{
|
||||
var eacp = new Eacp(8, 48);
|
||||
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
eacp.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
Assert.True(eacp.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_DominantCycle_WithinRange()
|
||||
{
|
||||
var eacp = new Eacp(8, 48);
|
||||
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
eacp.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
// Dominant cycle should be within the specified range
|
||||
Assert.InRange(eacp.DominantCycle, 8, 48);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_NormalizedPower_BetweenZeroAndOne()
|
||||
{
|
||||
var eacp = new Eacp(8, 48);
|
||||
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
eacp.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
Assert.InRange(eacp.NormalizedPower, 0, 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_InitialValue_NearMidpoint()
|
||||
{
|
||||
var eacp = new Eacp(8, 48);
|
||||
|
||||
// First update should return near midpoint of range
|
||||
var result = eacp.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
|
||||
// Initial dominant cycle starts at (8+48)/2 = 28
|
||||
Assert.True(result.Value >= 8 && result.Value <= 48);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Bar Correction Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewTrue_AdvancesState()
|
||||
{
|
||||
var eacp = new Eacp(8, 48);
|
||||
|
||||
eacp.Update(new TValue(DateTime.UtcNow, 100.0), isNew: true);
|
||||
var first = eacp.Last.Value;
|
||||
|
||||
eacp.Update(new TValue(DateTime.UtcNow.AddSeconds(1), 110.0), isNew: true);
|
||||
var second = eacp.Last.Value;
|
||||
|
||||
// Values should potentially differ
|
||||
Assert.True(double.IsFinite(first) && double.IsFinite(second));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewFalse_ReplacesCurrentBar()
|
||||
{
|
||||
var eacp = new Eacp(8, 48);
|
||||
|
||||
// Build some history
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
eacp.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + Math.Sin(i * 0.1) * 10), isNew: true);
|
||||
}
|
||||
|
||||
eacp.Update(new TValue(DateTime.UtcNow.AddSeconds(100), 110.0), isNew: true);
|
||||
var beforeCorrection = eacp.Last.Value;
|
||||
|
||||
// Correct the bar with a different value
|
||||
eacp.Update(new TValue(DateTime.UtcNow.AddSeconds(100), 90.0), isNew: false);
|
||||
var afterCorrection = eacp.Last.Value;
|
||||
|
||||
// Values should differ after correction
|
||||
Assert.True(double.IsFinite(beforeCorrection) && double.IsFinite(afterCorrection));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_MultipleCorrections_RestoresToSnapshot()
|
||||
{
|
||||
var eacp = new Eacp(8, 48);
|
||||
|
||||
// Build some history
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
eacp.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + i), isNew: true);
|
||||
}
|
||||
|
||||
// Add a new bar
|
||||
eacp.Update(new TValue(DateTime.UtcNow.AddSeconds(100), 150.0), isNew: true);
|
||||
var originalValue = eacp.Last.Value;
|
||||
|
||||
// Correct multiple times
|
||||
eacp.Update(new TValue(DateTime.UtcNow.AddSeconds(100), 160.0), isNew: false);
|
||||
eacp.Update(new TValue(DateTime.UtcNow.AddSeconds(100), 140.0), isNew: false);
|
||||
eacp.Update(new TValue(DateTime.UtcNow.AddSeconds(100), 150.0), isNew: false);
|
||||
var restoredValue = eacp.Last.Value;
|
||||
|
||||
Assert.Equal(originalValue, restoredValue, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reset Tests
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var eacp = new Eacp(8, 48);
|
||||
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
eacp.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + i));
|
||||
}
|
||||
|
||||
Assert.True(eacp.IsHot);
|
||||
|
||||
eacp.Reset();
|
||||
|
||||
Assert.False(eacp.IsHot);
|
||||
Assert.Equal(default, eacp.Last);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_AllowsReuse()
|
||||
{
|
||||
var eacp = new Eacp(8, 48);
|
||||
|
||||
// First run
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
eacp.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + Math.Sin(i * 0.1) * 10));
|
||||
}
|
||||
var firstResult = eacp.Last.Value;
|
||||
|
||||
eacp.Reset();
|
||||
|
||||
// Second run with same data
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
eacp.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + Math.Sin(i * 0.1) * 10));
|
||||
}
|
||||
var secondResult = eacp.Last.Value;
|
||||
|
||||
Assert.Equal(firstResult, secondResult, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region NaN/Infinity Handling Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_NaN_UsesLastValidValue()
|
||||
{
|
||||
var eacp = new Eacp(8, 48);
|
||||
|
||||
eacp.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
eacp.Update(new TValue(DateTime.UtcNow.AddSeconds(1), double.NaN));
|
||||
|
||||
Assert.True(double.IsFinite(eacp.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Infinity_UsesLastValidValue()
|
||||
{
|
||||
var eacp = new Eacp(8, 48);
|
||||
|
||||
eacp.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
eacp.Update(new TValue(DateTime.UtcNow.AddSeconds(1), double.PositiveInfinity));
|
||||
|
||||
Assert.True(double.IsFinite(eacp.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_NegativeInfinity_UsesLastValidValue()
|
||||
{
|
||||
var eacp = new Eacp(8, 48);
|
||||
|
||||
eacp.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
eacp.Update(new TValue(DateTime.UtcNow.AddSeconds(1), double.NegativeInfinity));
|
||||
|
||||
Assert.True(double.IsFinite(eacp.Last.Value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Consistency Tests
|
||||
|
||||
[Theory]
|
||||
[InlineData(42)]
|
||||
[InlineData(123)]
|
||||
[InlineData(999)]
|
||||
public void Update_StreamingMatchesBatch(int seed)
|
||||
{
|
||||
const int minPeriod = 8;
|
||||
const int maxPeriod = 48;
|
||||
const int dataLen = 200;
|
||||
|
||||
var gbm = new GBM(seed: seed);
|
||||
var bars = gbm.Fetch(dataLen, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Streaming
|
||||
var streaming = new Eacp(minPeriod, maxPeriod);
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
streaming.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
// Batch via TSeries
|
||||
var tSeries = new TSeries();
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
tSeries.Add(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
var batch = Eacp.Calculate(tSeries, minPeriod, maxPeriod);
|
||||
|
||||
// Compare last values
|
||||
Assert.Equal(batch[^1].Value, streaming.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_MatchesStreaming()
|
||||
{
|
||||
const int minPeriod = 8;
|
||||
const int maxPeriod = 48;
|
||||
const int dataLen = 200;
|
||||
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(dataLen, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Streaming
|
||||
var streaming = new Eacp(minPeriod, maxPeriod);
|
||||
var streamingResults = new double[dataLen];
|
||||
for (int i = 0; i < dataLen; i++)
|
||||
{
|
||||
streaming.Update(new TValue(bars[i].Time, bars[i].Close));
|
||||
streamingResults[i] = streaming.Last.Value;
|
||||
}
|
||||
|
||||
// Batch
|
||||
double[] source = new double[dataLen];
|
||||
double[] batchResults = new double[dataLen];
|
||||
for (int i = 0; i < dataLen; i++)
|
||||
{
|
||||
source[i] = bars[i].Close;
|
||||
}
|
||||
|
||||
Eacp.Batch(source, batchResults, minPeriod, maxPeriod);
|
||||
|
||||
// Compare all values
|
||||
for (int i = 0; i < dataLen; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i], batchResults[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Span API Tests
|
||||
|
||||
[Fact]
|
||||
public void Batch_ValidatesLengthMismatch()
|
||||
{
|
||||
double[] source = new double[100];
|
||||
double[] output = new double[50];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Eacp.Batch(source, output, 8, 48));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_ValidatesMinPeriod()
|
||||
{
|
||||
double[] source = new double[100];
|
||||
double[] output = new double[100];
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => Eacp.Batch(source, output, 2, 48));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_ValidatesMaxPeriod()
|
||||
{
|
||||
double[] source = new double[100];
|
||||
double[] output = new double[100];
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => Eacp.Batch(source, output, 8, 8));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_EmptyArrays_NoException()
|
||||
{
|
||||
double[] source = [];
|
||||
double[] output = [];
|
||||
|
||||
var ex = Record.Exception(() => Eacp.Batch(source, output, 8, 48));
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_HandlesNaN()
|
||||
{
|
||||
double[] source = { 100, 101, double.NaN, 103, 104, 105, 106, 107, 108, 109 };
|
||||
double[] output = new double[10];
|
||||
|
||||
Eacp.Batch(source, output, 3, 8);
|
||||
|
||||
foreach (double v in output)
|
||||
{
|
||||
Assert.True(double.IsFinite(v));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Chaining Tests
|
||||
|
||||
[Fact]
|
||||
public void Chaining_PropagatesUpdates()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var eacp = new Eacp(source, 8, 48);
|
||||
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
source.Add(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + Math.Sin(i * 0.1) * 10));
|
||||
}
|
||||
|
||||
Assert.True(eacp.IsHot);
|
||||
Assert.True(double.IsFinite(eacp.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Chaining_MultipleIndicators()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var eacp1 = new Eacp(source, 8, 48);
|
||||
var eacp2 = new Eacp(source, 12, 60);
|
||||
|
||||
for (int i = 0; i < 300; i++)
|
||||
{
|
||||
source.Add(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + Math.Sin(i * 0.1) * 10));
|
||||
}
|
||||
|
||||
// Both should have values
|
||||
Assert.True(double.IsFinite(eacp1.Last.Value));
|
||||
Assert.True(double.IsFinite(eacp2.Last.Value));
|
||||
|
||||
// Different ranges should produce different results
|
||||
Assert.NotEqual(eacp1.Last.Value, eacp2.Last.Value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Parameter Behavior Tests
|
||||
|
||||
[Theory]
|
||||
[InlineData(3, 20)]
|
||||
[InlineData(8, 48)]
|
||||
[InlineData(12, 100)]
|
||||
public void Update_DifferentRanges_ProducesValidResults(int minPeriod, int maxPeriod)
|
||||
{
|
||||
var eacp = new Eacp(minPeriod, maxPeriod);
|
||||
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
eacp.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
Assert.True(eacp.IsHot);
|
||||
Assert.InRange(eacp.DominantCycle, minPeriod, maxPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_EnhanceFalse_ProducesValidResults()
|
||||
{
|
||||
var eacp = new Eacp(8, 48, avgLength: 3, enhance: false);
|
||||
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
eacp.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
Assert.True(eacp.IsHot);
|
||||
Assert.InRange(eacp.DominantCycle, 8, 48);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(3)]
|
||||
[InlineData(5)]
|
||||
[InlineData(10)]
|
||||
public void Update_DifferentAvgLength_ProducesValidResults(int avgLength)
|
||||
{
|
||||
var eacp = new Eacp(8, 48, avgLength: avgLength);
|
||||
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
eacp.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
Assert.True(eacp.IsHot);
|
||||
Assert.InRange(eacp.DominantCycle, 8, 48);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for EACP (Ehlers Autocorrelation Periodogram).
|
||||
/// EACP is Ehlers' proprietary indicator not commonly implemented in trading libraries
|
||||
/// (TA-Lib, Skender, Tulip), so validation is done against mathematical properties
|
||||
/// and known theoretical results based on the original PineScript implementation.
|
||||
/// </summary>
|
||||
public class EacpValidationTests
|
||||
{
|
||||
private const double Tolerance = 1e-9;
|
||||
|
||||
#region Mathematical Property Validation
|
||||
|
||||
[Fact]
|
||||
public void Validation_ConstantSeries_DominantCycleWithinRange()
|
||||
{
|
||||
// For constant input, autocorrelation is undefined but the algorithm
|
||||
// should still produce a value within the valid range
|
||||
var eacp = new Eacp(8, 48, 3, true);
|
||||
|
||||
for (int i = 0; i < 500; i++)
|
||||
{
|
||||
eacp.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0));
|
||||
}
|
||||
|
||||
Assert.InRange(eacp.DominantCycle, 8, 48);
|
||||
Assert.InRange(eacp.NormalizedPower, 0.0, 1.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_SineWave_DetectsPeriod()
|
||||
{
|
||||
// EACP should detect the dominant period in a sine wave
|
||||
const int knownPeriod = 20;
|
||||
var eacp = new Eacp(8, 48, 3, true);
|
||||
|
||||
// Generate sine wave with known period
|
||||
for (int i = 0; i < 500; i++)
|
||||
{
|
||||
double price = 100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / knownPeriod);
|
||||
eacp.Update(new TValue(DateTime.UtcNow.AddSeconds(i), price));
|
||||
}
|
||||
|
||||
// Dominant cycle should be close to the known period
|
||||
// Allow 20% tolerance due to filter lag and warmup effects
|
||||
double tolerance = knownPeriod * 0.3;
|
||||
Assert.InRange(eacp.DominantCycle, knownPeriod - tolerance, knownPeriod + tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_MultipleCycles_DetectsDominant()
|
||||
{
|
||||
// When multiple cycles are present, EACP should detect the dominant one
|
||||
var eacp = new Eacp(8, 48, 3, true);
|
||||
|
||||
// Generate signal with dominant 16-period cycle and weaker 32-period cycle
|
||||
for (int i = 0; i < 500; i++)
|
||||
{
|
||||
double cycle16 = 10.0 * Math.Sin(2.0 * Math.PI * i / 16.0); // Stronger
|
||||
double cycle32 = 5.0 * Math.Sin(2.0 * Math.PI * i / 32.0); // Weaker
|
||||
double price = 100.0 + cycle16 + cycle32;
|
||||
eacp.Update(new TValue(DateTime.UtcNow.AddSeconds(i), price));
|
||||
}
|
||||
|
||||
// Should detect the dominant cycle (16) rather than the weaker one
|
||||
Assert.InRange(eacp.DominantCycle, 12, 24);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_NormalizedPower_BoundedZeroToOne()
|
||||
{
|
||||
// Normalized power should always be between 0 and 1
|
||||
var eacp = new Eacp(8, 48, 3, true);
|
||||
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
eacp.Update(new TValue(bar.Time, bar.Close));
|
||||
Assert.InRange(eacp.NormalizedPower, 0.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PineScript Formula Verification
|
||||
|
||||
[Fact]
|
||||
public void Validation_HighPassFilter_CoefficientsCorrect()
|
||||
{
|
||||
// Verify high-pass filter coefficient calculation
|
||||
// alphaHP = (cos(angle) + sin(angle) - 1) / cos(angle)
|
||||
// where angle = sqrt(2) * PI / maxPeriod
|
||||
|
||||
const int maxPeriod = 48;
|
||||
double angle = Math.Sqrt(2.0) * Math.PI / maxPeriod;
|
||||
double expectedAlphaHP = (Math.Cos(angle) + Math.Sin(angle) - 1.0) / Math.Cos(angle);
|
||||
|
||||
// Verify the calculation is within expected range
|
||||
Assert.InRange(expectedAlphaHP, 0.0, 1.0);
|
||||
|
||||
// The indicator should use this coefficient
|
||||
var eacp = new Eacp(8, maxPeriod);
|
||||
Assert.True(eacp.Name.Contains("48", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_SuperSmootherFilter_CoefficientsCorrect()
|
||||
{
|
||||
// Verify super-smoother filter coefficient calculation
|
||||
// a1 = exp(-sqrt(2) * PI / minPeriod)
|
||||
// b1 = 2 * a1 * cos(sqrt(2) * PI / minPeriod)
|
||||
// c2 = b1, c3 = -(a1^2), c1 = 1 - c2 - c3
|
||||
|
||||
const int minPeriod = 8;
|
||||
double a1 = Math.Exp(-Math.Sqrt(2.0) * Math.PI / minPeriod);
|
||||
double b1 = 2.0 * a1 * Math.Cos(Math.Sqrt(2.0) * Math.PI / minPeriod);
|
||||
double c2 = b1;
|
||||
double c3 = -(a1 * a1);
|
||||
double c1 = 1.0 - c2 - c3;
|
||||
|
||||
// Coefficients should sum to approximately 1 (with IIR feedback)
|
||||
Assert.True(a1 > 0 && a1 < 1, "a1 should be between 0 and 1");
|
||||
Assert.True(c1 > 0, "c1 should be positive");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_PowerDecayFactor_Calculation()
|
||||
{
|
||||
// Verify power decay factor calculation
|
||||
// k = 10^(-0.15 / (maxPeriod - minPeriod))
|
||||
|
||||
const int minPeriod = 8;
|
||||
const int maxPeriod = 48;
|
||||
double diff = maxPeriod - minPeriod;
|
||||
double expectedK = Math.Pow(10.0, -0.15 / diff);
|
||||
|
||||
// k should be slightly less than 1 (decay factor)
|
||||
Assert.True(expectedK > 0.99 && expectedK < 1.0, $"k should be close to but less than 1, got {expectedK}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_EnhanceMode_CubicEmphasis()
|
||||
{
|
||||
// Enhance mode applies cubic emphasis (pwr^3)
|
||||
// This should make peaks more pronounced
|
||||
|
||||
var eacpEnhanced = new Eacp(8, 48, 3, enhance: true);
|
||||
var eacpNormal = new Eacp(8, 48, 3, enhance: false);
|
||||
|
||||
// Generate sine wave
|
||||
for (int i = 0; i < 300; i++)
|
||||
{
|
||||
double price = 100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 20.0);
|
||||
eacpEnhanced.Update(new TValue(DateTime.UtcNow.AddSeconds(i), price));
|
||||
eacpNormal.Update(new TValue(DateTime.UtcNow.AddSeconds(i), price));
|
||||
}
|
||||
|
||||
// Both should produce valid results
|
||||
Assert.InRange(eacpEnhanced.DominantCycle, 8, 48);
|
||||
Assert.InRange(eacpNormal.DominantCycle, 8, 48);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Streaming vs Batch Consistency
|
||||
|
||||
[Theory]
|
||||
[InlineData(42)]
|
||||
[InlineData(123)]
|
||||
[InlineData(999)]
|
||||
public void Validation_StreamingMatchesBatch(int seed)
|
||||
{
|
||||
const int minPeriod = 8;
|
||||
const int maxPeriod = 48;
|
||||
const int dataLen = 200;
|
||||
|
||||
var gbm = new GBM(seed: seed);
|
||||
var bars = gbm.Fetch(dataLen, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Streaming
|
||||
var streaming = new Eacp(minPeriod, maxPeriod);
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
streaming.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
// Batch via TSeries
|
||||
var tSeries = new TSeries();
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
tSeries.Add(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
var batch = Eacp.Calculate(tSeries, minPeriod, maxPeriod);
|
||||
|
||||
// Compare last values
|
||||
Assert.Equal(batch[^1].Value, streaming.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_SpanMatchesTSeries()
|
||||
{
|
||||
const int minPeriod = 8;
|
||||
const int maxPeriod = 48;
|
||||
const int dataLen = 200;
|
||||
|
||||
var gbm = new GBM(seed: 77);
|
||||
var bars = gbm.Fetch(dataLen, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// TSeries approach
|
||||
var tSeries = new TSeries();
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
tSeries.Add(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
var tSeriesResult = Eacp.Calculate(tSeries, minPeriod, maxPeriod);
|
||||
|
||||
// Span approach
|
||||
double[] source = new double[dataLen];
|
||||
double[] spanResult = new double[dataLen];
|
||||
for (int i = 0; i < dataLen; i++)
|
||||
{
|
||||
source[i] = bars[i].Close;
|
||||
}
|
||||
|
||||
Eacp.Batch(source, spanResult, minPeriod, maxPeriod);
|
||||
|
||||
// Compare all values
|
||||
for (int i = 0; i < dataLen; i++)
|
||||
{
|
||||
Assert.Equal(tSeriesResult[i].Value, spanResult[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Different Parameter Combinations
|
||||
|
||||
[Theory]
|
||||
[InlineData(8, 48)]
|
||||
[InlineData(10, 60)]
|
||||
[InlineData(6, 30)]
|
||||
[InlineData(12, 100)]
|
||||
public void Validation_DifferentPeriodRanges_ConsistentResults(int minPeriod, int maxPeriod)
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var eacp = new Eacp(minPeriod, maxPeriod);
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
eacp.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
Assert.True(eacp.IsHot);
|
||||
Assert.InRange(eacp.DominantCycle, minPeriod, maxPeriod);
|
||||
Assert.InRange(eacp.NormalizedPower, 0.0, 1.0);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)] // Default: use lag length
|
||||
[InlineData(3)]
|
||||
[InlineData(10)]
|
||||
public void Validation_DifferentAvgLength_ConsistentResults(int avgLength)
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(300, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var eacp = new Eacp(8, 48, avgLength);
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
eacp.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
Assert.True(eacp.IsHot);
|
||||
Assert.InRange(eacp.DominantCycle, 8, 48);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Edge Cases
|
||||
|
||||
[Fact]
|
||||
public void Validation_VerySmallPrices_HandledCorrectly()
|
||||
{
|
||||
var eacp = new Eacp(8, 48);
|
||||
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
double price = 0.0001 + 0.00001 * Math.Sin(2.0 * Math.PI * i / 20.0);
|
||||
eacp.Update(new TValue(DateTime.UtcNow.AddSeconds(i), price));
|
||||
}
|
||||
|
||||
Assert.True(eacp.IsHot);
|
||||
Assert.InRange(eacp.DominantCycle, 8, 48);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_VeryLargePrices_HandledCorrectly()
|
||||
{
|
||||
var eacp = new Eacp(8, 48);
|
||||
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
double price = 1e10 + 1e9 * Math.Sin(2.0 * Math.PI * i / 20.0);
|
||||
eacp.Update(new TValue(DateTime.UtcNow.AddSeconds(i), price));
|
||||
}
|
||||
|
||||
Assert.True(eacp.IsHot);
|
||||
Assert.InRange(eacp.DominantCycle, 8, 48);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_HighVolatility_StableResults()
|
||||
{
|
||||
var eacp = new Eacp(8, 48);
|
||||
|
||||
var gbm = new GBM(seed: 42, sigma: 0.5); // High volatility
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
eacp.Update(new TValue(bar.Time, bar.Close));
|
||||
Assert.InRange(eacp.DominantCycle, 8, 48);
|
||||
Assert.InRange(eacp.NormalizedPower, 0.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_ZeroVariance_HandledGracefully()
|
||||
{
|
||||
// When all prices are identical, correlation is undefined
|
||||
// but the algorithm should still produce valid output
|
||||
var eacp = new Eacp(8, 48);
|
||||
|
||||
for (int i = 0; i < 300; i++)
|
||||
{
|
||||
eacp.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0));
|
||||
}
|
||||
|
||||
Assert.InRange(eacp.DominantCycle, 8, 48);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Autocorrelation Properties
|
||||
|
||||
[Fact]
|
||||
public void Validation_Autocorrelation_SineWaveHighCorrelation()
|
||||
{
|
||||
// A pure sine wave should have high autocorrelation at its period
|
||||
var eacp = new Eacp(8, 48, 3, true);
|
||||
|
||||
// Generate pure sine wave
|
||||
for (int i = 0; i < 300; i++)
|
||||
{
|
||||
double price = 100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 20.0);
|
||||
eacp.Update(new TValue(DateTime.UtcNow.AddSeconds(i), price));
|
||||
}
|
||||
|
||||
// Should have relatively high normalized power for a pure sine
|
||||
Assert.True(eacp.NormalizedPower > 0.1,
|
||||
$"Pure sine should have detectable power, got {eacp.NormalizedPower}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_RandomNoise_LowPower()
|
||||
{
|
||||
// Random noise should have low spectral power at any frequency
|
||||
var eacp = new Eacp(8, 48, 3, true);
|
||||
|
||||
var gbm = new GBM(seed: 42, mu: 0, sigma: 0.01); // Nearly pure noise
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
eacp.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
// For noise, dominant cycle detection is weak
|
||||
// Just verify it doesn't crash and produces valid output
|
||||
Assert.InRange(eacp.DominantCycle, 8, 48);
|
||||
Assert.InRange(eacp.NormalizedPower, 0.0, 1.0);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DFT Properties
|
||||
|
||||
[Fact]
|
||||
public void Validation_DFT_FrequencyResolution()
|
||||
{
|
||||
// DFT should distinguish between different frequencies
|
||||
const int period1 = 12;
|
||||
const int period2 = 36;
|
||||
|
||||
var eacp1 = new Eacp(8, 48);
|
||||
var eacp2 = new Eacp(8, 48);
|
||||
|
||||
// Generate two different sine waves
|
||||
for (int i = 0; i < 500; i++)
|
||||
{
|
||||
double price1 = 100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / period1);
|
||||
double price2 = 100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / period2);
|
||||
|
||||
eacp1.Update(new TValue(DateTime.UtcNow.AddSeconds(i), price1));
|
||||
eacp2.Update(new TValue(DateTime.UtcNow.AddSeconds(i), price2));
|
||||
}
|
||||
|
||||
// They should detect different dominant cycles
|
||||
double diff = Math.Abs(eacp1.DominantCycle - eacp2.DominantCycle);
|
||||
Assert.True(diff > 5, $"Should detect different cycles: {eacp1.DominantCycle} vs {eacp2.DominantCycle}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,450 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// EACP: Ehlers Autocorrelation Periodogram - Dominant cycle estimator using
|
||||
/// autocorrelation and spectral analysis via the Wiener-Khinchin theorem.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The Autocorrelation Periodogram indicator, developed by John Ehlers, estimates
|
||||
/// the dominant cycle period in price data by computing autocorrelation coefficients
|
||||
/// and transforming them to the frequency domain using DFT principles.
|
||||
///
|
||||
/// Algorithm:
|
||||
/// 1. High-pass filter removes DC offset and low-frequency trend
|
||||
/// 2. Super-smoother filter reduces high-frequency noise
|
||||
/// 3. Pearson correlation coefficients computed for each lag
|
||||
/// 4. DFT converts correlation to power spectrum
|
||||
/// 5. Smoothed power spectrum identifies dominant frequency
|
||||
/// 6. Weighted average of high-power periods yields dominant cycle
|
||||
///
|
||||
/// Properties:
|
||||
/// - Returns estimated dominant cycle period
|
||||
/// - Also provides normalized power at dominant period
|
||||
/// - Enhance mode applies cubic emphasis to highlight peaks
|
||||
/// - Self-calibrating via adaptive maximum power tracking
|
||||
///
|
||||
/// Key Insight:
|
||||
/// Autocorrelation naturally detects periodicity as a signal correlates with
|
||||
/// its own lagged values. The Wiener-Khinchin theorem relates autocorrelation
|
||||
/// to spectral density, enabling frequency domain analysis.
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Eacp : AbstractBase
|
||||
{
|
||||
private readonly int _minPeriod;
|
||||
private readonly int _maxPeriod;
|
||||
private readonly int _avgLength;
|
||||
private readonly bool _enhance;
|
||||
|
||||
// Filter coefficients
|
||||
private readonly double _alphaHP;
|
||||
private readonly double _c1, _c2, _c3;
|
||||
private readonly double _k; // Power decay factor
|
||||
|
||||
// Buffers for autocorrelation and power spectrum
|
||||
private readonly double[] _corr;
|
||||
private readonly double[] _power;
|
||||
private readonly double[] _smooth;
|
||||
|
||||
// State for filters and output
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(
|
||||
double Price0, double Price1, double Price2,
|
||||
double Hp0, double Hp1, double Hp2,
|
||||
double Filt0, double Filt1, double Filt2,
|
||||
double Dom, double DomPower, double MaxPwr,
|
||||
int BarCount, double LastValidValue
|
||||
);
|
||||
|
||||
private State _s;
|
||||
private State _ps;
|
||||
|
||||
// History buffer for correlation calculation
|
||||
private readonly RingBuffer _filtHistory;
|
||||
|
||||
/// <summary>Gets the current dominant cycle period.</summary>
|
||||
public double DominantCycle => _s.Dom;
|
||||
|
||||
/// <summary>Gets the normalized power at the dominant cycle period (0-1).</summary>
|
||||
public double NormalizedPower => _s.DomPower;
|
||||
|
||||
public override bool IsHot => _s.BarCount >= WarmupPeriod;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Ehlers Autocorrelation Periodogram indicator.
|
||||
/// </summary>
|
||||
/// <param name="minPeriod">Minimum period to evaluate (must be >= 3).</param>
|
||||
/// <param name="maxPeriod">Maximum period to evaluate (must be > minPeriod).</param>
|
||||
/// <param name="avgLength">Averaging length for Pearson correlation (0 uses lag length).</param>
|
||||
/// <param name="enhance">Apply cubic emphasis to highlight dominant peaks.</param>
|
||||
public Eacp(int minPeriod = 8, int maxPeriod = 48, int avgLength = 3, bool enhance = true)
|
||||
{
|
||||
if (minPeriod < 3)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(minPeriod), "Min period must be at least 3.");
|
||||
}
|
||||
if (maxPeriod <= minPeriod)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(maxPeriod), "Max period must be greater than min period.");
|
||||
}
|
||||
if (avgLength < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(avgLength), "Average length must be non-negative.");
|
||||
}
|
||||
|
||||
_minPeriod = minPeriod;
|
||||
_maxPeriod = maxPeriod;
|
||||
_avgLength = avgLength;
|
||||
_enhance = enhance;
|
||||
|
||||
int size = maxPeriod + 1;
|
||||
|
||||
// High-pass filter coefficient (tuned to maxPeriod)
|
||||
double angle = Math.Sqrt(2.0) * Math.PI / maxPeriod;
|
||||
_alphaHP = (Math.Cos(angle) + Math.Sin(angle) - 1.0) / Math.Cos(angle);
|
||||
|
||||
// Super-smoother filter coefficients (tuned to minPeriod)
|
||||
double a1 = Math.Exp(-Math.Sqrt(2.0) * Math.PI / minPeriod);
|
||||
double b1 = 2.0 * a1 * Math.Cos(Math.Sqrt(2.0) * Math.PI / minPeriod);
|
||||
_c2 = b1;
|
||||
_c3 = -(a1 * a1);
|
||||
_c1 = 1.0 - _c2 - _c3;
|
||||
|
||||
// Power decay factor
|
||||
double diff = maxPeriod - minPeriod;
|
||||
_k = diff > 0 ? Math.Pow(10.0, -0.15 / diff) : 1.0;
|
||||
|
||||
// Allocate buffers
|
||||
_corr = new double[size];
|
||||
_power = new double[size];
|
||||
_smooth = new double[size];
|
||||
_filtHistory = new RingBuffer(size + maxPeriod);
|
||||
|
||||
Name = $"Eacp({minPeriod},{maxPeriod})";
|
||||
WarmupPeriod = maxPeriod * 2;
|
||||
|
||||
// Initialize state
|
||||
double initialDom = (minPeriod + maxPeriod) * 0.5;
|
||||
_s = new State(0, 0, 0, 0, 0, 0, 0, 0, 0, initialDom, 0, 0, 0, 0);
|
||||
_ps = _s;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a chained Ehlers Autocorrelation Periodogram indicator.
|
||||
/// </summary>
|
||||
public Eacp(ITValuePublisher source, int minPeriod = 8, int maxPeriod = 48, int avgLength = 3, bool enhance = true)
|
||||
: this(minPeriod, maxPeriod, avgLength, enhance)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
source.Pub += HandleInput;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void HandleInput(object? sender, in TValueEventArgs e)
|
||||
{
|
||||
Update(e.Value, e.IsNew);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_ps = _s;
|
||||
_filtHistory.Snapshot();
|
||||
}
|
||||
else
|
||||
{
|
||||
_s = _ps;
|
||||
_filtHistory.Restore();
|
||||
}
|
||||
|
||||
var s = _s;
|
||||
|
||||
// Handle non-finite values
|
||||
double price = input.Value;
|
||||
if (!double.IsFinite(price))
|
||||
{
|
||||
price = s.LastValidValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
s = s with { LastValidValue = price };
|
||||
}
|
||||
|
||||
// Increment bar count
|
||||
int barCount = isNew ? s.BarCount + 1 : s.BarCount;
|
||||
|
||||
// Shift price history
|
||||
double price2 = s.Price1;
|
||||
double price1 = s.Price0;
|
||||
double price0 = price;
|
||||
|
||||
// High-pass filter: removes DC and low-frequency trend
|
||||
double hp2 = s.Hp1;
|
||||
double hp1 = s.Hp0;
|
||||
double coef = (1.0 - _alphaHP / 2.0);
|
||||
double hp0 = coef * coef * (price0 - 2.0 * price1 + price2)
|
||||
+ 2.0 * (1.0 - _alphaHP) * hp1
|
||||
- (1.0 - _alphaHP) * (1.0 - _alphaHP) * hp2;
|
||||
|
||||
// Super-smoother filter: removes high-frequency noise
|
||||
double filt2 = s.Filt1;
|
||||
double filt1 = s.Filt0;
|
||||
double filt0 = _c1 * (hp0 + hp1) * 0.5 + _c2 * filt1 + _c3 * filt2;
|
||||
|
||||
// Add filtered value to history buffer
|
||||
_filtHistory.Add(filt0);
|
||||
|
||||
// Compute autocorrelation for each lag
|
||||
ComputeAutocorrelation();
|
||||
|
||||
// Compute power spectrum via DFT
|
||||
ComputePowerSpectrum();
|
||||
|
||||
// Find dominant cycle
|
||||
var (dom, domPower, maxPwr) = FindDominantCycle(s.Dom, s.MaxPwr);
|
||||
|
||||
// Update state
|
||||
_s = new State(price0, price1, price2, hp0, hp1, hp2, filt0, filt1, filt2,
|
||||
dom, domPower, maxPwr, barCount, s.LastValidValue);
|
||||
|
||||
Last = new TValue(input.Time, dom);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Process each value
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
var result = Update(source[i]);
|
||||
vSpan[i] = result.Value;
|
||||
}
|
||||
source.Times.CopyTo(tSpan);
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void ComputeAutocorrelation()
|
||||
{
|
||||
int histCount = _filtHistory.Count;
|
||||
|
||||
for (int lag = 0; lag <= _maxPeriod; lag++)
|
||||
{
|
||||
if (lag < 2)
|
||||
{
|
||||
_corr[lag] = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
int window = _avgLength == 0 ? lag : _avgLength;
|
||||
if (window < 2)
|
||||
{
|
||||
window = 2;
|
||||
}
|
||||
|
||||
// Compute Pearson correlation coefficient
|
||||
double sx = 0, sy = 0, sxx = 0, syy = 0, sxy = 0;
|
||||
int valid = 0;
|
||||
|
||||
for (int k = 0; k < window && (lag + k) < histCount; k++)
|
||||
{
|
||||
double x = _filtHistory[histCount - 1 - k];
|
||||
double y = (lag + k) < histCount ? _filtHistory[histCount - 1 - lag - k] : 0;
|
||||
sx += x;
|
||||
sy += y;
|
||||
sxx += x * x;
|
||||
syy += y * y;
|
||||
sxy += x * y;
|
||||
valid++;
|
||||
}
|
||||
|
||||
double corrVal = 0;
|
||||
if (valid > 1)
|
||||
{
|
||||
double denomX = valid * sxx - sx * sx;
|
||||
double denomY = valid * syy - sy * sy;
|
||||
double denom = denomX * denomY;
|
||||
if (denom > 0)
|
||||
{
|
||||
corrVal = (valid * sxy - sx * sy) / Math.Sqrt(denom);
|
||||
}
|
||||
}
|
||||
|
||||
_corr[lag] = corrVal;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void ComputePowerSpectrum()
|
||||
{
|
||||
// DFT to convert correlation to power spectrum
|
||||
for (int period = _minPeriod; period <= _maxPeriod; period++)
|
||||
{
|
||||
double cosAcc = 0, sinAcc = 0;
|
||||
|
||||
for (int n = 2; n <= _maxPeriod; n++)
|
||||
{
|
||||
double angle = 2.0 * Math.PI * n / period;
|
||||
cosAcc += _corr[n] * Math.Cos(angle);
|
||||
sinAcc += _corr[n] * Math.Sin(angle);
|
||||
}
|
||||
|
||||
// Power = amplitude squared
|
||||
double sq = cosAcc * cosAcc + sinAcc * sinAcc;
|
||||
|
||||
// Smooth the power spectrum (EMA-like smoothing)
|
||||
_smooth[period] = 0.2 * sq + 0.8 * _smooth[period];
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private (double dom, double domPower, double maxPwr) FindDominantCycle(double prevDom, double prevMaxPwr)
|
||||
{
|
||||
// Find local maximum power
|
||||
double localMaxPwr = 0;
|
||||
for (int period = _minPeriod; period <= _maxPeriod; period++)
|
||||
{
|
||||
if (_smooth[period] > localMaxPwr)
|
||||
{
|
||||
localMaxPwr = _smooth[period];
|
||||
}
|
||||
}
|
||||
|
||||
// Adaptive maximum power tracking
|
||||
double maxPwr;
|
||||
if (localMaxPwr > prevMaxPwr)
|
||||
{
|
||||
maxPwr = localMaxPwr;
|
||||
}
|
||||
else
|
||||
{
|
||||
maxPwr = _k * prevMaxPwr;
|
||||
}
|
||||
|
||||
// Normalize power and apply enhancement
|
||||
double weighted = 0, sumWeight = 0, peakPwr = 0;
|
||||
for (int period = _minPeriod; period <= _maxPeriod; period++)
|
||||
{
|
||||
double pwr = maxPwr > 0 ? _smooth[period] / maxPwr : 0;
|
||||
if (_enhance)
|
||||
{
|
||||
pwr = pwr * pwr * pwr; // Cubic emphasis
|
||||
}
|
||||
_power[period] = pwr;
|
||||
|
||||
if (pwr > peakPwr)
|
||||
{
|
||||
peakPwr = pwr;
|
||||
}
|
||||
|
||||
if (pwr >= 0.5)
|
||||
{
|
||||
weighted += period * pwr;
|
||||
sumWeight += pwr;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate dominant cycle - use prevDom as fallback
|
||||
double baseDom = sumWeight >= 0.25 ? weighted / sumWeight : prevDom;
|
||||
|
||||
// Apply EMA smoothing (alpha = 0.2) - this is the PineScript formula
|
||||
// dom := alpha*(base-dom)+dom which equals dom + alpha*(base-dom)
|
||||
double dom = prevDom + 0.2 * (baseDom - prevDom);
|
||||
|
||||
// Ensure dom stays within bounds
|
||||
dom = Math.Clamp(dom, _minPeriod, _maxPeriod);
|
||||
|
||||
// Get power at dominant cycle - clamp to [0,1] for floating-point safety
|
||||
int domIdx = Math.Clamp((int)Math.Round(dom), _minPeriod, _maxPeriod);
|
||||
double domPower = Math.Clamp(_power[domIdx], 0.0, 1.0);
|
||||
|
||||
return (dom, domPower, maxPwr);
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
double initialDom = (_minPeriod + _maxPeriod) * 0.5;
|
||||
_s = new State(0, 0, 0, 0, 0, 0, 0, 0, 0, initialDom, 0, 0, 0, 0);
|
||||
_ps = _s;
|
||||
_filtHistory.Clear();
|
||||
Array.Clear(_corr);
|
||||
Array.Clear(_power);
|
||||
Array.Clear(_smooth);
|
||||
Last = default;
|
||||
}
|
||||
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
foreach (double value in source)
|
||||
{
|
||||
Update(new TValue(DateTime.UtcNow, value));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates EACP for a time series.
|
||||
/// </summary>
|
||||
public static TSeries Calculate(TSeries source, int minPeriod = 8, int maxPeriod = 48,
|
||||
int avgLength = 3, bool enhance = true)
|
||||
{
|
||||
var eacp = new Eacp(minPeriod, maxPeriod, avgLength, enhance);
|
||||
return eacp.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates EACP in-place using a pre-allocated output span.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Batch(ReadOnlySpan<double> source, Span<double> output,
|
||||
int minPeriod = 8, int maxPeriod = 48, int avgLength = 3, bool enhance = true)
|
||||
{
|
||||
if (source.Length != output.Length)
|
||||
{
|
||||
throw new ArgumentException("Source and output must have the same length", nameof(output));
|
||||
}
|
||||
|
||||
if (minPeriod < 3)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(minPeriod), "Min period must be at least 3.");
|
||||
}
|
||||
if (maxPeriod <= minPeriod)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(maxPeriod), "Max period must be greater than min period.");
|
||||
}
|
||||
|
||||
int len = source.Length;
|
||||
if (len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Use streaming implementation for batch (complex state management)
|
||||
var eacp = new Eacp(minPeriod, maxPeriod, avgLength, enhance);
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
var result = eacp.Update(new TValue(DateTime.UtcNow, source[i]));
|
||||
output[i] = result.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
+175
-133
@@ -1,182 +1,224 @@
|
||||
# EACP: Ehlers Autocorrelation Periodogram
|
||||
|
||||
## Overview and Purpose
|
||||
> "The autocorrelation periodogram uses the Wiener-Khinchin theorem to transform autocorrelation into spectral density, revealing the dominant cycle hidden within price noise."
|
||||
|
||||
Developed by John F. Ehlers (Technical Analysis of Stocks & Commodities, Sep 2016), the Ehlers Autocorrelation Periodogram (EACP) estimates the dominant market cycle by projecting normalized autocorrelation coefficients onto Fourier basis functions. The indicator blends a roofing filter (high-pass + Super Smoother) with a compact periodogram, yielding low-latency dominant cycle detection suitable for adaptive trading systems. Compared with Hilbert-based methods, the autocorrelation approach resists aliasing and maintains stability in noisy price data.
|
||||
The Ehlers Autocorrelation Periodogram (EACP) is a sophisticated cycle detection algorithm that estimates the dominant market cycle period by computing autocorrelation coefficients and transforming them to the frequency domain via discrete Fourier transform (DFT). Unlike simple period detectors, EACP leverages the mathematical relationship between autocorrelation and power spectral density to identify cyclical behavior even in noisy price data.
|
||||
|
||||
EACP answers a central question in cycle analysis: “What period currently dominates the market?” It prioritizes spectral power concentration, enabling downstream tools (adaptive moving averages, oscillators) to adjust responsively without the lag present in sliding-window techniques.
|
||||
## Historical Context
|
||||
|
||||
## Core Concepts
|
||||
John Ehlers introduced the Autocorrelation Periodogram in his work on digital signal processing applied to trading. The algorithm addresses a fundamental challenge: market cycles are not stationary, and their periods change over time. Traditional Fourier analysis assumes stationarity, making it poorly suited for adaptive cycle detection.
|
||||
|
||||
* **Roofing Filter:** High-pass plus Super Smoother combination removes low-frequency drift while limiting aliasing.
|
||||
* **Pearson Autocorrelation:** Computes normalized lag correlation to remove amplitude bias.
|
||||
* **Fourier Projection:** Sums cosine and sine terms of autocorrelation to approximate spectral energy.
|
||||
* **Gain Normalization:** Automatic gain control prevents stale peaks from dominating power estimates.
|
||||
* **Warmup Compensation:** Exponential correction guarantees valid output from the very first bar.
|
||||
Ehlers' insight was to use the Wiener-Khinchin theorem, which states that the autocorrelation function and power spectral density are Fourier transform pairs. By computing autocorrelation coefficients at various lags and transforming them via DFT, the algorithm produces a power spectrum that reveals dominant frequencies (cycle periods) in the data.
|
||||
|
||||
## Implementation Notes
|
||||
The implementation here follows Ehlers' PineScript version, which includes:
|
||||
- High-pass filtering to remove DC offset and low-frequency trends
|
||||
- Super-smoother filtering to reduce high-frequency noise
|
||||
- Pearson correlation for lag-based autocorrelation
|
||||
- DFT conversion to power spectrum
|
||||
- Adaptive maximum power tracking with decay
|
||||
- Optional cubic enhancement to sharpen spectral peaks
|
||||
|
||||
**This is not a strict implementation of the TASC September 2016 specification.** It is a more advanced evolution combining the core 2016 concept with techniques Ehlers introduced later. The fundamental Wiener-Khinchin theorem (power spectral density = Fourier transform of autocorrelation) is correctly implemented, but key implementation details differ:
|
||||
## Architecture & Physics
|
||||
|
||||
### Differences from Original 2016 TASC Article
|
||||
### 1. High-Pass Filter
|
||||
|
||||
1. **Dominant Cycle Calculation:**
|
||||
* **2016 TASC:** Uses peak-finding to identify the period with maximum power
|
||||
* **This Implementation:** Uses Center of Gravity (COG) weighted average over bins where power ≥ 0.5
|
||||
* **Rationale:** COG provides smoother transitions and reduces susceptibility to noise spikes
|
||||
The high-pass filter removes DC offset and low-frequency trend components that would otherwise dominate the autocorrelation:
|
||||
|
||||
2. **Roofing Filter:**
|
||||
* **2016 TASC:** Simple first-order high-pass filter
|
||||
* **This Implementation:** Canonical 2-pole high-pass with √2 factor followed by Super Smoother bandpass
|
||||
* **Formula:** `hp := (1-α/2)²·(p-2p[1]+p[2]) + 2(1-α)·hp[1] - (1-α)²·hp[2]`
|
||||
* **Rationale:** Evolved filtering provides better attenuation and phase characteristics
|
||||
$$
|
||||
\alpha_{HP} = \frac{\cos(\theta) + \sin(\theta) - 1}{\cos(\theta)}
|
||||
$$
|
||||
|
||||
3. **Normalized Power Reporting:**
|
||||
* **2016 TASC:** Reports peak power across all periods
|
||||
* **This Implementation:** Reports power specifically at the dominant period
|
||||
* **Rationale:** Provides more meaningful correlation between dominant cycle strength and normalized power
|
||||
where $\theta = \sqrt{2} \cdot \frac{\pi}{\text{maxPeriod}}$
|
||||
|
||||
4. **Automatic Gain Control (AGC):**
|
||||
* Uses decay factor `K = 10^(-0.15/diff)` where `diff = maxPeriod - minPeriod`
|
||||
* Ensures K < 1 for proper exponential decay of historical peaks
|
||||
* Prevents stale peaks from dominating current power estimates
|
||||
The filter is a second-order IIR:
|
||||
|
||||
### Performance Characteristics
|
||||
$$
|
||||
HP_t = (1 - \frac{\alpha_{HP}}{2})^2 (P_t - 2P_{t-1} + P_{t-2}) + 2(1 - \alpha_{HP})HP_{t-1} - (1 - \alpha_{HP})^2 HP_{t-2}
|
||||
$$
|
||||
|
||||
* **Complexity:** O(N²) where N = (maxPeriod - minPeriod)
|
||||
* **Implementation:** Uses `var` arrays with native PineScript historical operator `[offset]`
|
||||
* **Warmup:** Exponential compensation (§2 pattern) ensures valid output from bar 1
|
||||
### 2. Super-Smoother Filter
|
||||
|
||||
### Related Implementations
|
||||
The super-smoother removes high-frequency noise while preserving cyclical content:
|
||||
|
||||
This refined approach aligns with:
|
||||
* TradingView TASC 2025.02 implementation by blackcat1402
|
||||
* Modern Ehlers cycle analysis techniques post-2016
|
||||
* Evolved filtering methods from *Cycle Analytics for Traders*
|
||||
$$
|
||||
a_1 = e^{-\sqrt{2} \cdot \pi / \text{minPeriod}}
|
||||
$$
|
||||
|
||||
The code is mathematically sound and production-ready, representing a refined version of the autocorrelation periodogram concept rather than a literal translation of the 2016 article.
|
||||
$$
|
||||
b_1 = 2 a_1 \cos\left(\sqrt{2} \cdot \frac{\pi}{\text{minPeriod}}\right)
|
||||
$$
|
||||
|
||||
## Common Settings and Parameters
|
||||
$$
|
||||
c_1 = 1 - c_2 - c_3, \quad c_2 = b_1, \quad c_3 = -a_1^2
|
||||
$$
|
||||
|
||||
| Parameter | Default | Function | When to Adjust |
|
||||
| ------ | ------ | ------ | ------ |
|
||||
| Min Period | 8 | Lower bound of candidate cycles | Increase to ignore microstructure noise; decrease for scalping. |
|
||||
| Max Period | 48 | Upper bound of candidate cycles | Increase for swing analysis; decrease for intraday focus. |
|
||||
| Autocorrelation Length | 3 | Averaging window for Pearson correlation | Set to 0 to match lag, or enlarge for smoother spectra. |
|
||||
| Enhance Resolution | true | Cubic emphasis to highlight peaks | Disable when a flatter spectrum is desired for diagnostics. |
|
||||
$$
|
||||
F_t = \frac{c_1}{2}(HP_t + HP_{t-1}) + c_2 F_{t-1} + c_3 F_{t-2}
|
||||
$$
|
||||
|
||||
**Pro Tip:** Keep `(maxPeriod - minPeriod)` ≤ 64 to control $O(n^2)$ inner loops and maintain responsiveness on lower timeframes.
|
||||
### 3. Pearson Autocorrelation
|
||||
|
||||
## Calculation and Mathematical Foundation
|
||||
For each lag $\ell$ from 2 to maxPeriod, compute the Pearson correlation between the filtered series and its lagged version:
|
||||
|
||||
**Explanation:**
|
||||
1. Apply roofing filter to `source` using coefficients $\alpha_1$, $a_1$, $b_1$, $c_1$, $c_2$, $c_3$.
|
||||
2. For each lag $L$ compute Pearson correlation $r_L$ over window $M$ (default $L$).
|
||||
3. For each period $p$, project onto Fourier basis:
|
||||
$C_p=\sum_{n=2}^{N} r_n \cos\left(\frac{2\pi n}{p}\right)$ and $S_p=\sum_{n=2}^{N} r_n \sin\left(\frac{2\pi n}{p}\right)$.
|
||||
4. Power $P_p=C_p^2+S_p^2$, smoothed then normalized via adaptive peak tracking.
|
||||
5. Dominant cycle $D=\frac{\sum p\,\tilde P_p}{\sum \tilde P_p}$ over bins where $\tilde P_p≥0.5$, warmup-compensated.
|
||||
$$
|
||||
r_\ell = \frac{n \sum x_i y_i - \sum x_i \sum y_i}{\sqrt{(n \sum x_i^2 - (\sum x_i)^2)(n \sum y_i^2 - (\sum y_i)^2)}}
|
||||
$$
|
||||
|
||||
**Technical formula:**
|
||||
```
|
||||
Step 1: hp_t = ((1-α₁)/2)(src_t - src_{t-1}) + α₁ hp_{t-1}
|
||||
Step 2: filt_t = c₁(hp_t + hp_{t-1})/2 + c₂ filt_{t-1} + c₃ filt_{t-2}
|
||||
Step 3: r_L = (M Σxy - Σx Σy) / √[(M Σx² - (Σx)²)(M Σy² - (Σy)²)]
|
||||
Step 4: P_p = (Σ_{n=2}^{N} r_n cos(2πn/p))² + (Σ_{n=2}^{N} r_n sin(2πn/p))²
|
||||
Step 5: D = Σ_{p∈Ω} p · ĤP_p / Σ_{p∈Ω} ĤP_p with warmup compensation
|
||||
```
|
||||
where $x_i = F_{t-i}$ and $y_i = F_{t-\ell-i}$ for $i \in [0, \text{window})$.
|
||||
|
||||
> 🔍 **Technical Note:** Warmup uses $c = 1 / (1 - (1 - \alpha)^{k})$ to scale early-cycle estimates, preventing low values during initial bars.
|
||||
### 4. Discrete Fourier Transform
|
||||
|
||||
## Interpretation Details
|
||||
Convert autocorrelation to power spectrum via DFT:
|
||||
|
||||
* **Primary Dominant Cycle:**
|
||||
* High $D$ (e.g., > 30) implies slow regime; adaptive MAs should lengthen.
|
||||
* Low $D$ (e.g., < 15) signals rapid oscillations; shorten lookback windows.
|
||||
$$
|
||||
\text{cosAcc}_p = \sum_{n=2}^{\text{maxPeriod}} r_n \cos\left(\frac{2\pi n}{p}\right)
|
||||
$$
|
||||
|
||||
* **Normalized Power:**
|
||||
* Values > 0.8 indicate strong cycle confidence; consider cyclical strategies.
|
||||
* Values < 0.3 warn of flat spectra; favor trend or volatility approaches.
|
||||
$$
|
||||
\text{sinAcc}_p = \sum_{n=2}^{\text{maxPeriod}} r_n \sin\left(\frac{2\pi n}{p}\right)
|
||||
$$
|
||||
|
||||
* **Regime Shifts:**
|
||||
* Rapid drop in $D$ alongside rising power often precedes volatility expansion.
|
||||
* Divergence between $D$ and price swings may highlight upcoming breakouts.
|
||||
$$
|
||||
\text{Power}_p = \text{cosAcc}_p^2 + \text{sinAcc}_p^2
|
||||
$$
|
||||
|
||||
## Limitations and Considerations
|
||||
### 5. Smoothed Power Spectrum
|
||||
|
||||
* **Spectral Leakage:** Limited lag range can smear peaks during abrupt volatility shifts.
|
||||
* **O(n²) Segment:** Although constrained (≤ 60 loops), wide period spans increase computation.
|
||||
* **Stationarity Assumption:** Autocorrelation presumes quasi-stationary cycles; regime changes reduce accuracy.
|
||||
* **Latency in Noise:** Even with roofing, extremely noisy assets may require higher `avgLength`.
|
||||
* **Downtrend Bias:** Negative trends may clip high-pass output; ensure preprocessing retains signal.
|
||||
Apply EMA-style smoothing to the power spectrum:
|
||||
|
||||
$$
|
||||
S_p = 0.2 \cdot \text{Power}_p^2 + 0.8 \cdot S_{p,\text{prev}}
|
||||
$$
|
||||
|
||||
### 6. Adaptive Maximum Power Tracking
|
||||
|
||||
Track the maximum power with decay to normalize the spectrum:
|
||||
|
||||
$$
|
||||
\text{MaxPwr}_t = \begin{cases}
|
||||
\text{localMax} & \text{if localMax} > \text{MaxPwr}_{t-1} \\
|
||||
K \cdot \text{MaxPwr}_{t-1} & \text{otherwise}
|
||||
\end{cases}
|
||||
$$
|
||||
|
||||
where $K = 10^{-0.15 / (\text{maxPeriod} - \text{minPeriod})}$
|
||||
|
||||
### 7. Dominant Cycle Extraction
|
||||
|
||||
Normalize power and optionally apply cubic enhancement:
|
||||
|
||||
$$
|
||||
\text{pwr}_p = \frac{S_p}{\text{MaxPwr}_t}
|
||||
$$
|
||||
|
||||
$$
|
||||
\text{pwr}_p = \text{pwr}_p^3 \quad \text{(if enhance = true)}
|
||||
$$
|
||||
|
||||
Compute weighted average of periods with sufficient power:
|
||||
|
||||
$$
|
||||
\text{Dom}_t = \frac{\sum_{p:\text{pwr}_p \geq 0.5} p \cdot \text{pwr}_p}{\sum_{p:\text{pwr}_p \geq 0.5} \text{pwr}_p}
|
||||
$$
|
||||
|
||||
Apply smoothing:
|
||||
|
||||
$$
|
||||
\text{Dom}_t = 0.2 \cdot (\text{baseDom} - \text{Dom}_{t-1}) + \text{Dom}_{t-1}
|
||||
$$
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Wiener-Khinchin Theorem
|
||||
|
||||
The theorem establishes that for a wide-sense stationary process:
|
||||
|
||||
$$
|
||||
S(\omega) = \mathcal{F}\{R(\tau)\}
|
||||
$$
|
||||
|
||||
where $S(\omega)$ is the power spectral density and $R(\tau)$ is the autocorrelation function. This means peaks in the autocorrelation at lag $\tau$ correspond to peaks in the power spectrum at frequency $\omega = 2\pi/\tau$.
|
||||
|
||||
### Filter Design Rationale
|
||||
|
||||
The high-pass filter cutoff at maxPeriod ensures cycles longer than the detection range are attenuated. The super-smoother cutoff at minPeriod removes noise at frequencies higher than the detection range. This creates a bandpass effect that isolates cycles within [minPeriod, maxPeriod].
|
||||
|
||||
### Cubic Enhancement
|
||||
|
||||
The cubic function $f(x) = x^3$ sharpens peaks because:
|
||||
- Values near 1 remain close to 1: $0.9^3 = 0.729$
|
||||
- Values near 0 become much smaller: $0.5^3 = 0.125$
|
||||
|
||||
This creates better separation between dominant and spurious cycles.
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Streaming Mode, per Bar)
|
||||
### Operation Count (Streaming Mode, Scalar)
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| ADD/SUB | ~N² | 1 | ~N² |
|
||||
| MUL | ~N² | 3 | ~3N² |
|
||||
| DIV | ~N | 15 | ~15N |
|
||||
| SQRT | ~N | 15 | ~15N |
|
||||
| COS | N² | 40 | 40N² |
|
||||
| SIN | N² | 40 | 40N² |
|
||||
| **Total** | **~4N²** | — | **~84N² cycles** |
|
||||
| HP filter (MUL/ADD) | 8 | 3 | 24 |
|
||||
| SS filter (MUL/ADD) | 6 | 3 | 18 |
|
||||
| Autocorrelation loop | O(maxPeriod × avgLength) | 5 | ~1200 |
|
||||
| DFT loop | O(maxPeriod²) | 10 | ~23000 |
|
||||
| Power normalization | O(maxPeriod) | 3 | ~150 |
|
||||
| Weighted average | O(maxPeriod) | 5 | ~250 |
|
||||
| **Total** | — | — | **~25000 cycles** |
|
||||
|
||||
*Where N = maxPeriod - minPeriod (default 40)*
|
||||
The DFT loop dominates at O(maxPeriod²). For maxPeriod=48, this is ~2300 iterations per bar.
|
||||
|
||||
**Default (N=40):** ~134,400 cycles per bar (dominated by trig functions)
|
||||
### Batch Mode
|
||||
|
||||
**Breakdown:**
|
||||
- Roofing filter (HP + SSF): ~20 cycles
|
||||
- Autocorrelation (N lags): ~4N² for Pearson calculations
|
||||
- Fourier projection (N² iterations): 80N² cycles (COS + SIN)
|
||||
- Power + normalization: ~30N cycles
|
||||
Due to the recursive nature of autocorrelation and DFT, SIMD optimization is limited to:
|
||||
- Vectorized DFT inner products (modest gains)
|
||||
- Parallel power normalization
|
||||
|
||||
### Complexity Analysis
|
||||
|
||||
| Mode | Complexity | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| Streaming | O(N²) | Nested loops over lags × periods |
|
||||
| Batch | O(m×N²) | m = bars, N = period range |
|
||||
|
||||
**Memory**: ~3N×8 bytes (autocorrelation + power arrays)
|
||||
|
||||
### SIMD Analysis
|
||||
|
||||
| Optimization | Applicable | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| AVX2 vectorization | Partial | Fourier sums vectorizable across lags |
|
||||
| FMA | ✅ | Accumulation: `r × cos + sum` pattern |
|
||||
| Batch parallelism | Limited | Each bar depends on filtered history |
|
||||
|
||||
**Optimization Notes:** Trig functions dominate cost. Consider:
|
||||
- Precomputed trig tables for fixed period range
|
||||
- SVML vectorized sin/cos for ~4× speedup
|
||||
- Reduce N by narrowing period search range
|
||||
Expected speedup: ~1.3x with AVX2 for DFT vectorization.
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **Accuracy** | 9/10 | Wiener-Khinchin theorem mathematically sound |
|
||||
| **Timeliness** | 6/10 | Spectral analysis inherently lagging |
|
||||
| **Overshoot** | 8/10 | COG averaging smooths cycle estimates |
|
||||
| **Smoothness** | 7/10 | Enhanced resolution can create jumps |
|
||||
| **Accuracy** | 8/10 | Good cycle detection for clean signals |
|
||||
| **Timeliness** | 6/10 | Requires warmup; smoothing adds lag |
|
||||
| **Overshoot** | 7/10 | Bounded output range prevents extremes |
|
||||
| **Smoothness** | 8/10 | EMA smoothing reduces jitter |
|
||||
| **Noise Rejection** | 7/10 | Dual filtering provides good denoising |
|
||||
|
||||
## Validation
|
||||
|
||||
EACP is a proprietary Ehlers indicator not commonly found in standard libraries.
|
||||
|
||||
| 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** | ✅ | Reference implementation |
|
||||
|
||||
Validation is performed against:
|
||||
- Mathematical properties (bounded output, sine wave detection)
|
||||
- PineScript formula verification
|
||||
- Streaming vs batch consistency
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Warmup Period**: EACP requires approximately 2×maxPeriod bars to stabilize. During warmup, the dominant cycle estimate is biased toward the midpoint of [minPeriod, maxPeriod]. Always check `IsHot` before using results.
|
||||
|
||||
2. **Computational Cost**: The O(maxPeriod²) DFT is expensive. For real-time applications with maxPeriod > 100, consider reducing the period range or increasing the bar interval.
|
||||
|
||||
3. **Parameter Sensitivity**: The minPeriod/maxPeriod range must bracket the expected cycle. If the true cycle is outside this range, detection will fail. Start with a wide range (8-48) and narrow based on market characteristics.
|
||||
|
||||
4. **Enhance Mode**: While cubic enhancement sharpens peaks, it can also suppress weak-but-valid cycles. Disable enhancement when analyzing low-amplitude cycles or noisy data.
|
||||
|
||||
5. **Memory Footprint**: The indicator maintains O(maxPeriod) buffers for correlation, power, and smoothed power. Each instance consumes ~2KB for default parameters.
|
||||
|
||||
6. **Non-Stationary Markets**: Markets without clear cyclical behavior will produce unstable dominant cycle estimates. Use normalized power as a confidence metric: high power indicates strong cyclical behavior.
|
||||
|
||||
## References
|
||||
|
||||
* Ehlers, J. F. (2016). “Past Market Cycles.” *Technical Analysis of Stocks & Commodities*, 34(9), 52-55.
|
||||
* Thinkorswim Learning Center. “Ehlers Autocorrelation Periodogram.”
|
||||
* Fab MacCallini. “autocorrPeriodogram.R.” GitHub repository.
|
||||
* QuantStrat TradeR Blog. “Autocorrelation Periodogram for Adaptive Lookbacks.”
|
||||
* TradingView Script by blackcat1402. “Ehlers Autocorrelation Periodogram (Updated).”
|
||||
|
||||
``` mcp
|
||||
Validation Sources:
|
||||
Patterns: §2, §3, §7, §21
|
||||
Wolfram: "Wiener-Khinchin theorem"
|
||||
External: "Thinkorswim Ehlers Autocorrelation Periodogram","fabmaccallini autocorrPeriodogram","QuantStrat Autocorrelation Periodogram","TradingView blackcat Autocorrelation Periodogram"
|
||||
API: ref-tools confirmed input.source/int/bool usage, plot defaults
|
||||
Planning: phases=design,warmup,validation,docs
|
||||
- Ehlers, J.F. (2013). "Cycle Analytics for Traders." Wiley.
|
||||
- Ehlers, J.F. "Autocorrelation Periodogram." Technical Analysis of Stocks & Commodities.
|
||||
- Wiener, N. (1930). "Generalized Harmonic Analysis." Acta Mathematica.
|
||||
- Khinchin, A.Y. (1934). "Korrelationstheorie der stationären stochastischen Prozesse." Mathematische Annalen.
|
||||
Reference in New Issue
Block a user