mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-25 13:58:04 +00:00
docs: remove C# Implementation Considerations sections, clean up temp scripts, reorganize test files
- Remove 'C# Implementation Considerations' sections from 34 indicator .md files - Delete 29 temp PowerShell scripts (_fix_mojibake.ps1, _hex_scan.ps1, etc.) - Move test files into tests/ subdirectories for consistent project structure - Add trader-focused bullet points to indicator documentation
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,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.Batch(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,450 @@
|
||||
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.Batch(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.Batch(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}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Eacp_Correction_Recomputes()
|
||||
{
|
||||
var ind = new Eacp(8, 48, 3, true);
|
||||
var t0 = new DateTime(946_684_800_000_000_0L, DateTimeKind.Utc);
|
||||
|
||||
// Build state well past warmup
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
ind.Update(new TValue(t0.AddMinutes(i),
|
||||
100.0 + (10.0 * Math.Sin(2.0 * Math.PI * i / 20.0))), isNew: true);
|
||||
}
|
||||
|
||||
// Anchor bar
|
||||
var anchorTime = t0.AddMinutes(100);
|
||||
const double anchorPrice = 105.5;
|
||||
ind.Update(new TValue(anchorTime, anchorPrice), isNew: true);
|
||||
double anchorResult = ind.Last.Value;
|
||||
|
||||
// Correction with a dramatically different price — recompute must yield different result
|
||||
ind.Update(new TValue(anchorTime, anchorPrice * 10.0), isNew: false);
|
||||
Assert.NotEqual(anchorResult, ind.Last.Value);
|
||||
|
||||
// Correction back to original price — must exactly restore original result
|
||||
ind.Update(new TValue(anchorTime, anchorPrice), isNew: false);
|
||||
Assert.Equal(anchorResult, ind.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user