mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-26 06:18:05 +00:00
feat: add EPA (Ehlers Phasor Analysis) indicator - TASC Nov 2022
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Quantower.Tests;
|
||||
|
||||
public class EpaIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_DefaultParameters()
|
||||
{
|
||||
var indicator = new EpaIndicator();
|
||||
Assert.Equal(28, indicator.Period);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinHistoryDepths_IsZero()
|
||||
{
|
||||
Assert.Equal(0, EpaIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShortName_ContainsPeriod()
|
||||
{
|
||||
var indicator = new EpaIndicator { Period = 20 };
|
||||
Assert.Contains("20", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialize_DoesNotThrow()
|
||||
{
|
||||
var indicator = new EpaIndicator();
|
||||
var ex = Record.Exception(() => indicator.Initialize());
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessUpdate_Historical_DoesNotThrow()
|
||||
{
|
||||
var indicator = new EpaIndicator();
|
||||
indicator.Initialize();
|
||||
indicator.HistoricalData.AddBar(
|
||||
open: 100, high: 105, low: 95, close: 102, volume: 1000,
|
||||
time: DateTime.UtcNow);
|
||||
var ex = Record.Exception(() =>
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)));
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessUpdate_NewBar_DoesNotThrow()
|
||||
{
|
||||
var indicator = new EpaIndicator();
|
||||
indicator.Initialize();
|
||||
indicator.HistoricalData.AddBar(
|
||||
open: 100, high: 105, low: 95, close: 102, volume: 1000,
|
||||
time: DateTime.UtcNow);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
indicator.HistoricalData.AddBar(
|
||||
open: 102, high: 107, low: 97, close: 104, volume: 1100,
|
||||
time: DateTime.UtcNow.AddDays(1));
|
||||
var ex = Record.Exception(() =>
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar)));
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessUpdate_Tick_DoesNotThrow()
|
||||
{
|
||||
var indicator = new EpaIndicator();
|
||||
indicator.Initialize();
|
||||
indicator.HistoricalData.AddBar(
|
||||
open: 100, high: 105, low: 95, close: 102, volume: 1000,
|
||||
time: DateTime.UtcNow);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
var ex = Record.Exception(() =>
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick)));
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SourceCodeLink_IsNotEmpty()
|
||||
{
|
||||
var indicator = new EpaIndicator();
|
||||
Assert.False(string.IsNullOrEmpty(indicator.SourceCodeLink));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultipleHistoricalBars_DoNotThrow()
|
||||
{
|
||||
var indicator = new EpaIndicator { Period = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(
|
||||
open: 100 + i, high: 105 + i, low: 95 + i, close: 102 + i,
|
||||
volume: 1000 + i * 10,
|
||||
time: DateTime.UtcNow.AddDays(i));
|
||||
var ex = Record.Exception(() =>
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)));
|
||||
Assert.Null(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CustomPeriod_InitializesCorrectly()
|
||||
{
|
||||
var indicator = new EpaIndicator { Period = 14 };
|
||||
indicator.Initialize();
|
||||
Assert.Contains("14", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentSources_DoNotThrow()
|
||||
{
|
||||
foreach (var sourceType in new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close })
|
||||
{
|
||||
var indicator = new EpaIndicator { Source = sourceType };
|
||||
indicator.Initialize();
|
||||
indicator.HistoricalData.AddBar(
|
||||
open: 100, high: 105, low: 95, close: 102, volume: 1000,
|
||||
time: DateTime.UtcNow);
|
||||
var ex = Record.Exception(() =>
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)));
|
||||
Assert.Null(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,487 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class EpaTests
|
||||
{
|
||||
private static TSeries MakeSeries(int count = 500)
|
||||
{
|
||||
var rng = new Random(42);
|
||||
var s = new TSeries();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
s.Add(new TValue(DateTime.UtcNow.AddDays(i), 100 + rng.NextDouble() * 10));
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// ── Constructor ────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Ctor_DefaultPeriod_Is28()
|
||||
{
|
||||
var epa = new Epa();
|
||||
Assert.Equal("Epa(28)", epa.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_CustomPeriod_SetsName()
|
||||
{
|
||||
var epa = new Epa(period: 14);
|
||||
Assert.Equal("Epa(14)", epa.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_Period1_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Epa(period: 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_Period0_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Epa(period: 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_NegativePeriod_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Epa(period: -5));
|
||||
}
|
||||
|
||||
// ── Basic Calculation ──────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Update_FirstBar_ReturnsZeroAngle()
|
||||
{
|
||||
var epa = new Epa();
|
||||
var result = epa.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.Equal(0.0, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_AfterWarmup_ReturnsFiniteAngle()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
var s = MakeSeries(50);
|
||||
TValue last = default;
|
||||
foreach (var tv in s)
|
||||
{
|
||||
last = epa.Update(tv);
|
||||
}
|
||||
Assert.True(double.IsFinite(last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Angle_IsSetAfterUpdate()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
var s = MakeSeries(20);
|
||||
foreach (var tv in s)
|
||||
{
|
||||
epa.Update(tv);
|
||||
}
|
||||
Assert.True(double.IsFinite(epa.Angle));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DerivedPeriod_IsFiniteAfterWarmup()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
var s = MakeSeries(30);
|
||||
foreach (var tv in s)
|
||||
{
|
||||
epa.Update(tv);
|
||||
}
|
||||
Assert.True(double.IsFinite(epa.DerivedPeriod));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrendState_IsValid()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
var s = MakeSeries(50);
|
||||
foreach (var tv in s)
|
||||
{
|
||||
epa.Update(tv);
|
||||
}
|
||||
Assert.InRange(epa.TrendState, -1, 1);
|
||||
}
|
||||
|
||||
// ── State / Bar Correction ─────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void BarCorrection_UpdateWithIsNewFalse_RestoresState()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
var s = MakeSeries(20);
|
||||
|
||||
// Process first 19 bars
|
||||
for (int i = 0; i < 19; i++)
|
||||
{
|
||||
epa.Update(s[i]);
|
||||
}
|
||||
|
||||
// Process bar 20 (new)
|
||||
epa.Update(s[19], isNew: true);
|
||||
double angleAfterNew = epa.Angle;
|
||||
|
||||
// Correct bar 20 (not new) with same value
|
||||
epa.Update(s[19], isNew: false);
|
||||
double angleAfterCorrection = epa.Angle;
|
||||
|
||||
Assert.Equal(angleAfterNew, angleAfterCorrection, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BarCorrection_DifferentValue_ProducesDifferentResult()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
var s = MakeSeries(20);
|
||||
|
||||
for (int i = 0; i < 19; i++)
|
||||
{
|
||||
epa.Update(s[i]);
|
||||
}
|
||||
|
||||
// New bar
|
||||
epa.Update(s[19], isNew: true);
|
||||
|
||||
// Correct with very different value
|
||||
epa.Update(new TValue(s[19].Time, s[19].Value + 50), isNew: false);
|
||||
double angle2 = epa.Angle;
|
||||
|
||||
// May or may not be different due to monotonic constraint, but should be finite
|
||||
Assert.True(double.IsFinite(angle2));
|
||||
}
|
||||
|
||||
// ── Warmup / IsHot ─────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void IsHot_FalseBeforeWarmup()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
epa.Update(new TValue(DateTime.UtcNow.AddDays(i), 100 + i));
|
||||
}
|
||||
Assert.False(epa.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_TrueAtWarmup()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
epa.Update(new TValue(DateTime.UtcNow.AddDays(i), 100 + i));
|
||||
}
|
||||
Assert.True(epa.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_EqualsPeriod()
|
||||
{
|
||||
var epa = new Epa(period: 20);
|
||||
Assert.Equal(20, epa.WarmupPeriod);
|
||||
}
|
||||
|
||||
// ── Robustness ─────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void NaN_Input_DoesNotCorrupt()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
var s = MakeSeries(20);
|
||||
foreach (var tv in s)
|
||||
{
|
||||
epa.Update(tv);
|
||||
}
|
||||
|
||||
// Feed NaN
|
||||
epa.Update(new TValue(DateTime.UtcNow.AddDays(100), double.NaN));
|
||||
Assert.True(double.IsFinite(epa.Angle));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_Input_DoesNotCorrupt()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
var s = MakeSeries(20);
|
||||
foreach (var tv in s)
|
||||
{
|
||||
epa.Update(tv);
|
||||
}
|
||||
epa.Update(new TValue(DateTime.UtcNow.AddDays(100), double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(epa.Angle));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstantInput_Angle_IsFinite()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
epa.Update(new TValue(DateTime.UtcNow.AddDays(i), 42.0));
|
||||
}
|
||||
Assert.True(double.IsFinite(epa.Angle));
|
||||
}
|
||||
|
||||
// ── Reset ──────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
var s = MakeSeries(30);
|
||||
foreach (var tv in s)
|
||||
{
|
||||
epa.Update(tv);
|
||||
}
|
||||
Assert.True(epa.IsHot);
|
||||
|
||||
epa.Reset();
|
||||
Assert.False(epa.IsHot);
|
||||
Assert.Equal(0.0, epa.Angle);
|
||||
Assert.Equal(0.0, epa.DerivedPeriod);
|
||||
Assert.Equal(0, epa.TrendState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ProducesSameResultsOnReprocess()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
var s = MakeSeries(50);
|
||||
|
||||
foreach (var tv in s)
|
||||
{
|
||||
epa.Update(tv);
|
||||
}
|
||||
double angle1 = epa.Angle;
|
||||
|
||||
epa.Reset();
|
||||
foreach (var tv in s)
|
||||
{
|
||||
epa.Update(tv);
|
||||
}
|
||||
double angle2 = epa.Angle;
|
||||
|
||||
Assert.Equal(angle1, angle2, precision: 10);
|
||||
}
|
||||
|
||||
// ── Consistency: 4 API modes ───────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void AllModes_Consistent()
|
||||
{
|
||||
var s = MakeSeries(200);
|
||||
int period = 14;
|
||||
|
||||
// Mode 1: streaming
|
||||
var epa1 = new Epa(period);
|
||||
foreach (var tv in s)
|
||||
{
|
||||
epa1.Update(tv);
|
||||
}
|
||||
|
||||
// Mode 2: Update(TSeries)
|
||||
var epa2 = new Epa(period);
|
||||
var ts2 = epa2.Update(s);
|
||||
|
||||
// Mode 3: Batch(TSeries)
|
||||
var ts3 = Epa.Batch(s, period);
|
||||
|
||||
// Mode 4: Batch(Span)
|
||||
double[] src = new double[s.Count];
|
||||
double[] dst = new double[s.Count];
|
||||
for (int i = 0; i < s.Count; i++)
|
||||
{
|
||||
src[i] = s[i].Value;
|
||||
}
|
||||
Epa.Batch(src, dst, period);
|
||||
|
||||
Assert.Equal(ts2[^1].Value, ts3[^1].Value, precision: 10);
|
||||
Assert.Equal(ts2[^1].Value, dst[^1], precision: 10);
|
||||
Assert.Equal(epa1.Angle, ts2[^1].Value, precision: 10);
|
||||
}
|
||||
|
||||
// ── Batch(TSeries) ─────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Batch_TSeries_SameLengthAsSource()
|
||||
{
|
||||
var s = MakeSeries(100);
|
||||
var result = Epa.Batch(s);
|
||||
Assert.Equal(s.Count, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_TSeries_EmptySource_ReturnsEmpty()
|
||||
{
|
||||
var result = Epa.Batch(new TSeries());
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
// ── Batch(Span) ────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_ProducesFiniteOutput()
|
||||
{
|
||||
double[] src = [100, 101, 102, 103, 104, 103, 102, 101, 100, 99, 98, 99, 100, 101, 102];
|
||||
double[] dst = new double[src.Length];
|
||||
Epa.Batch(src, dst, period: 5);
|
||||
foreach (double v in dst)
|
||||
{
|
||||
Assert.True(double.IsFinite(v));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_MismatchedLength_Throws()
|
||||
{
|
||||
double[] src = new double[10];
|
||||
double[] dst = new double[5];
|
||||
Assert.Throws<ArgumentException>(() => Epa.Batch(src, dst));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_InvalidPeriod_Throws()
|
||||
{
|
||||
double[] src = new double[10];
|
||||
double[] dst = new double[10];
|
||||
Assert.Throws<ArgumentException>(() => Epa.Batch(src, dst, period: 0));
|
||||
}
|
||||
|
||||
// ── Calculate factory ──────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsResultsAndIndicator()
|
||||
{
|
||||
var s = MakeSeries(50);
|
||||
var (results, indicator) = Epa.Calculate(s, period: 10);
|
||||
Assert.Equal(s.Count, results.Count);
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.Equal(indicator.Angle, results[^1].Value, precision: 10);
|
||||
}
|
||||
|
||||
// ── PubSub (chaining) ──────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void PubSub_ReceivesEvents()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var epa = new Epa(source, period: 10);
|
||||
int eventCount = 0;
|
||||
epa.Pub += (object? sender, in TValueEventArgs args) => eventCount++;
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
source.Add(new TValue(DateTime.UtcNow.AddDays(i), 100 + i));
|
||||
}
|
||||
|
||||
Assert.Equal(20, eventCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PubSub_NullSource_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => new Epa(null!, period: 10));
|
||||
}
|
||||
|
||||
// ── Prime ──────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Prime_WarmUpIndicator()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
double[] data = new double[20];
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
data[i] = 100 + i * 0.5;
|
||||
}
|
||||
epa.Prime(data);
|
||||
Assert.True(epa.IsHot);
|
||||
}
|
||||
|
||||
// ── EPA-specific behavior ──────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void SineWave_ProducesVaryingAngle()
|
||||
{
|
||||
var epa = new Epa(period: 20);
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double price = 100 + 10 * Math.Sin(2 * Math.PI * i / 20.0);
|
||||
epa.Update(new TValue(DateTime.UtcNow.AddDays(i), price));
|
||||
}
|
||||
// With a matching sine wave, angle should advance
|
||||
Assert.True(double.IsFinite(epa.Angle));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DerivedPeriod_ClampedTo60()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
var s = MakeSeries(200);
|
||||
foreach (var tv in s)
|
||||
{
|
||||
epa.Update(tv);
|
||||
Assert.True(epa.DerivedPeriod <= 60.0,
|
||||
$"DerivedPeriod {epa.DerivedPeriod} exceeds max 60");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrendState_OnlyValidValues()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
var s = MakeSeries(200);
|
||||
foreach (var tv in s)
|
||||
{
|
||||
epa.Update(tv);
|
||||
Assert.True(epa.TrendState == -1 || epa.TrendState == 0 || epa.TrendState == 1,
|
||||
$"Invalid TrendState: {epa.TrendState}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentPeriod_DifferentResults()
|
||||
{
|
||||
var s = MakeSeries(100);
|
||||
|
||||
var epa10 = new Epa(period: 10);
|
||||
var epa28 = new Epa(period: 28);
|
||||
|
||||
foreach (var tv in s)
|
||||
{
|
||||
epa10.Update(tv);
|
||||
epa28.Update(tv);
|
||||
}
|
||||
|
||||
// Different periods should generally produce different angles
|
||||
// (not guaranteed for all data, but very likely with random data)
|
||||
Assert.NotEqual(epa10.Angle, epa28.Angle);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TSeries_MatchesStreaming()
|
||||
{
|
||||
var s = MakeSeries(100);
|
||||
int period = 14;
|
||||
|
||||
// Streaming
|
||||
var epa1 = new Epa(period);
|
||||
foreach (var tv in s)
|
||||
{
|
||||
epa1.Update(tv);
|
||||
}
|
||||
|
||||
// Update(TSeries)
|
||||
var epa2 = new Epa(period);
|
||||
_ = epa2.Update(s);
|
||||
|
||||
Assert.Equal(epa1.Angle, epa2.Angle, precision: 10);
|
||||
Assert.Equal(epa1.DerivedPeriod, epa2.DerivedPeriod, precision: 10);
|
||||
Assert.Equal(epa1.TrendState, epa2.TrendState);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class EpaValidationTests
|
||||
{
|
||||
// ── Pearson Correlation Properties ──────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ConstantPrice_RealAndAngle_AreZero()
|
||||
{
|
||||
// Constant price has zero variance → correlation = 0 → angle = 0
|
||||
var epa = new Epa(period: 10);
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
epa.Update(new TValue(DateTime.UtcNow.AddDays(i), 50.0));
|
||||
}
|
||||
Assert.Equal(0.0, epa.Angle);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PerfectCosineInput_HighCorrelation()
|
||||
{
|
||||
// Price that exactly matches cos wave at the indicator period should yield |Real| near 1
|
||||
int period = 20;
|
||||
var epa = new Epa(period: period);
|
||||
double maxAngle = double.MinValue;
|
||||
|
||||
for (int i = 0; i < period * 4; i++)
|
||||
{
|
||||
double price = 100 + 10 * Math.Cos(2 * Math.PI * i / period);
|
||||
epa.Update(new TValue(DateTime.UtcNow.AddDays(i), price));
|
||||
if (epa.IsHot && Math.Abs(epa.Angle) > Math.Abs(maxAngle))
|
||||
{
|
||||
maxAngle = epa.Angle;
|
||||
}
|
||||
}
|
||||
// The angle should move significantly when price matches the reference cosine
|
||||
Assert.True(double.IsFinite(maxAngle));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PerfectSineInput_AngleAdvances()
|
||||
{
|
||||
// A sine wave at the indicator period should produce advancing angle.
|
||||
// The angle wraps at the 360° boundary (e.g. ~180° → ~-162°), which is
|
||||
// the expected wraparound compensation behavior.
|
||||
int period = 20;
|
||||
var epa = new Epa(period: period);
|
||||
var angles = new List<double>();
|
||||
|
||||
for (int i = 0; i < period * 3; i++)
|
||||
{
|
||||
double price = 100 + 10 * Math.Sin(2 * Math.PI * i / period);
|
||||
epa.Update(new TValue(DateTime.UtcNow.AddDays(i), price));
|
||||
if (epa.IsHot)
|
||||
{
|
||||
angles.Add(epa.Angle);
|
||||
}
|
||||
}
|
||||
|
||||
// Angle should advance or wrap around (decrease > 300° is a valid wraparound)
|
||||
Assert.True(angles.Count > 0);
|
||||
int advances = 0;
|
||||
for (int i = 1; i < angles.Count; i++)
|
||||
{
|
||||
double delta = angles[i] - angles[i - 1];
|
||||
if (delta >= -0.001)
|
||||
{
|
||||
advances++; // Normal advancement or hold
|
||||
}
|
||||
else if (delta < -300.0)
|
||||
{
|
||||
advances++; // Valid 360° wraparound
|
||||
}
|
||||
// else: backward movement in non-wrap region — allowed by Ehlers' exceptions
|
||||
}
|
||||
// Most transitions should be advancing or wrapping
|
||||
Assert.True(advances > angles.Count / 2,
|
||||
$"Expected majority of angle transitions to advance, got {advances}/{angles.Count}");
|
||||
}
|
||||
|
||||
// ── DerivedPeriod Properties ───────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void DerivedPeriod_AlwaysClampedTo60()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
var rng = new Random(123);
|
||||
|
||||
for (int i = 0; i < 500; i++)
|
||||
{
|
||||
double price = 100 + rng.NextDouble() * 20;
|
||||
epa.Update(new TValue(DateTime.UtcNow.AddDays(i), price));
|
||||
Assert.True(epa.DerivedPeriod <= 60.0,
|
||||
$"DerivedPeriod {epa.DerivedPeriod} > 60 at bar {i}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DerivedPeriod_NonNegative()
|
||||
{
|
||||
var epa = new Epa(period: 14);
|
||||
var rng = new Random(456);
|
||||
|
||||
for (int i = 0; i < 300; i++)
|
||||
{
|
||||
double price = 100 + rng.NextDouble() * 10;
|
||||
epa.Update(new TValue(DateTime.UtcNow.AddDays(i), price));
|
||||
Assert.True(epa.DerivedPeriod >= 0.0,
|
||||
$"DerivedPeriod {epa.DerivedPeriod} < 0 at bar {i}");
|
||||
}
|
||||
}
|
||||
|
||||
// ── TrendState Properties ──────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void TrendState_OnlyValidValues_AllBars()
|
||||
{
|
||||
var epa = new Epa(period: 14);
|
||||
var rng = new Random(789);
|
||||
|
||||
for (int i = 0; i < 500; i++)
|
||||
{
|
||||
double price = 100 + rng.NextDouble() * 10;
|
||||
epa.Update(new TValue(DateTime.UtcNow.AddDays(i), price));
|
||||
Assert.True(epa.TrendState >= -1 && epa.TrendState <= 1,
|
||||
$"Invalid TrendState {epa.TrendState} at bar {i}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrendState_HasVariation()
|
||||
{
|
||||
// Over a long enough series with varying data, trend state should not be constant
|
||||
var epa = new Epa(period: 10);
|
||||
var states = new HashSet<int>();
|
||||
var rng = new Random(42);
|
||||
|
||||
for (int i = 0; i < 500; i++)
|
||||
{
|
||||
double price = 100 + rng.NextDouble() * 20 + 5 * Math.Sin(2 * Math.PI * i / 20.0);
|
||||
epa.Update(new TValue(DateTime.UtcNow.AddDays(i), price));
|
||||
if (epa.IsHot)
|
||||
{
|
||||
states.Add(epa.TrendState);
|
||||
}
|
||||
}
|
||||
// Should have at least 2 different states
|
||||
Assert.True(states.Count >= 2,
|
||||
$"Expected at least 2 distinct states, got {states.Count}: [{string.Join(",", states)}]");
|
||||
}
|
||||
|
||||
// ── Deterministic Reproducibility ──────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Deterministic_SameInput_SameOutput()
|
||||
{
|
||||
var rng1 = new Random(42);
|
||||
var rng2 = new Random(42);
|
||||
var epa1 = new Epa(period: 14);
|
||||
var epa2 = new Epa(period: 14);
|
||||
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
double p1 = 100 + rng1.NextDouble() * 10;
|
||||
double p2 = 100 + rng2.NextDouble() * 10;
|
||||
epa1.Update(new TValue(DateTime.UtcNow.AddDays(i), p1));
|
||||
epa2.Update(new TValue(DateTime.UtcNow.AddDays(i), p2));
|
||||
}
|
||||
|
||||
Assert.Equal(epa1.Angle, epa2.Angle, precision: 14);
|
||||
Assert.Equal(epa1.DerivedPeriod, epa2.DerivedPeriod, precision: 14);
|
||||
Assert.Equal(epa1.TrendState, epa2.TrendState);
|
||||
}
|
||||
|
||||
// ── Consistency: Batch/Streaming/Span ──────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void StreamingVsBatch_Match()
|
||||
{
|
||||
var rng = new Random(42);
|
||||
int n = 200, period = 14;
|
||||
double[] prices = new double[n];
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
prices[i] = 100 + rng.NextDouble() * 10;
|
||||
}
|
||||
|
||||
// Streaming
|
||||
var epa = new Epa(period);
|
||||
double[] streamAngles = new double[n];
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
var r = epa.Update(new TValue(DateTime.UtcNow.AddDays(i), prices[i]));
|
||||
streamAngles[i] = r.Value;
|
||||
}
|
||||
|
||||
// Span batch
|
||||
double[] spanAngles = new double[n];
|
||||
Epa.Batch(prices, spanAngles, period);
|
||||
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
Assert.Equal(streamAngles[i], spanAngles[i], precision: 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchTSeries_MatchesStreaming()
|
||||
{
|
||||
var rng = new Random(42);
|
||||
int n = 200, period = 14;
|
||||
var ts = new TSeries();
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
ts.Add(new TValue(DateTime.UtcNow.AddDays(i), 100 + rng.NextDouble() * 10));
|
||||
}
|
||||
|
||||
// Streaming
|
||||
var epa = new Epa(period);
|
||||
foreach (var tv in ts)
|
||||
{
|
||||
epa.Update(tv);
|
||||
}
|
||||
|
||||
// Batch(TSeries)
|
||||
var batchResult = Epa.Batch(ts, period);
|
||||
|
||||
Assert.Equal(epa.Angle, batchResult[^1].Value, precision: 10);
|
||||
}
|
||||
|
||||
// ── Reset/Reprocess ────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ResetReprocess_MatchesOriginal()
|
||||
{
|
||||
var rng = new Random(42);
|
||||
int n = 100, period = 14;
|
||||
var epa = new Epa(period);
|
||||
double[] prices = new double[n];
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
prices[i] = 100 + rng.NextDouble() * 10;
|
||||
}
|
||||
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
epa.Update(new TValue(DateTime.UtcNow.AddDays(i), prices[i]));
|
||||
}
|
||||
double angle1 = epa.Angle;
|
||||
double dp1 = epa.DerivedPeriod;
|
||||
int ts1 = epa.TrendState;
|
||||
|
||||
epa.Reset();
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
epa.Update(new TValue(DateTime.UtcNow.AddDays(i), prices[i]));
|
||||
}
|
||||
|
||||
Assert.Equal(angle1, epa.Angle, precision: 14);
|
||||
Assert.Equal(dp1, epa.DerivedPeriod, precision: 14);
|
||||
Assert.Equal(ts1, epa.TrendState);
|
||||
}
|
||||
|
||||
// ── Period Sensitivity ─────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void DifferentPeriods_DifferentAngle()
|
||||
{
|
||||
var rng = new Random(42);
|
||||
var epa10 = new Epa(period: 10);
|
||||
var epa28 = new Epa(period: 28);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double price = 100 + rng.NextDouble() * 10;
|
||||
var tv = new TValue(DateTime.UtcNow.AddDays(i), price);
|
||||
epa10.Update(tv);
|
||||
epa28.Update(tv);
|
||||
}
|
||||
|
||||
Assert.NotEqual(epa10.Angle, epa28.Angle);
|
||||
}
|
||||
|
||||
// ── Finite Output for All Bars ─────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void AllOutputs_AlwaysFinite()
|
||||
{
|
||||
var epa = new Epa(period: 14);
|
||||
var rng = new Random(42);
|
||||
|
||||
for (int i = 0; i < 500; i++)
|
||||
{
|
||||
double price = 100 + rng.NextDouble() * 10;
|
||||
epa.Update(new TValue(DateTime.UtcNow.AddDays(i), price));
|
||||
Assert.True(double.IsFinite(epa.Angle), $"Non-finite Angle at bar {i}");
|
||||
Assert.True(double.IsFinite(epa.DerivedPeriod), $"Non-finite DerivedPeriod at bar {i}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user