mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-16 17:48:05 +00:00
207 lines
6.7 KiB
C#
207 lines
6.7 KiB
C#
namespace QuanTAlib.Tests;
|
||||
|
|
|
|||
|
|
public class EvwmaValidationTests
|
|||
|
|
{
|
|||
|
|
private readonly ValidationTestData _data;
|
|||
|
|
|
|||
|
|
public EvwmaValidationTests()
|
|||
|
|
{
|
|||
|
|
_data = new ValidationTestData();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ============ External Library Validation ============
|
|||
|
|
// EVWMA is not available in standard libraries (Skender, TA-Lib, Tulip, Ooples).
|
|||
|
|
// Validation is performed via internal consistency and known-value tests.
|
|||
|
|
|
|||
|
|
[Fact]
|
|||
|
|
public void Evwma_NotAvailable_Skender()
|
|||
|
|
{
|
|||
|
|
// Skender.Stock.Indicators does not have EVWMA
|
|||
|
|
Assert.True(true, "EVWMA is not available in Skender");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[Fact]
|
|||
|
|
public void Evwma_NotAvailable_Talib()
|
|||
|
|
{
|
|||
|
|
// TA-Lib does not have EVWMA
|
|||
|
|
Assert.True(true, "EVWMA is not available in TA-Lib");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[Fact]
|
|||
|
|
public void Evwma_NotAvailable_Tulip()
|
|||
|
|
{
|
|||
|
|
// Tulip does not have EVWMA
|
|||
|
|
Assert.True(true, "EVWMA is not available in Tulip");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[Fact]
|
|||
|
|
public void Evwma_NotAvailable_Ooples()
|
|||
|
|
{
|
|||
|
|
// Ooples does not have EVWMA
|
|||
|
|
Assert.True(true, "EVWMA is not available in Ooples");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ============ Internal Consistency Tests ============
|
|||
|
|
|
|||
|
|
[Fact]
|
|||
|
|
public void Evwma_Streaming_Matches_Batch()
|
|||
|
|
{
|
|||
|
|
int period = 20;
|
|||
|
|
|
|||
|
|
// Streaming
|
|||
|
|
var evwma = new Evwma(period);
|
|||
|
|
var streamingValues = new List<double>();
|
|||
|
|
foreach (var bar in _data.Bars)
|
|||
|
|
{
|
|||
|
|
streamingValues.Add(evwma.Update(bar).Value);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Batch
|
|||
|
|
var batchResult = Evwma.Batch(_data.Bars, period);
|
|||
|
|
var batchValues = batchResult.Values.ToArray();
|
|||
|
|
|
|||
|
|
ValidationHelper.VerifyData(streamingValues.ToArray(), batchValues, 0, 100, 1e-10);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[Fact]
|
|||
|
|
public void Evwma_Span_Matches_Streaming()
|
|||
|
|
{
|
|||
|
|
int period = 20;
|
|||
|
|
|
|||
|
|
// Streaming
|
|||
|
|
var evwma = new Evwma(period);
|
|||
|
|
var streamingValues = new List<double>();
|
|||
|
|
foreach (var bar in _data.Bars)
|
|||
|
|
{
|
|||
|
|
streamingValues.Add(evwma.Update(bar).Value);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Span
|
|||
|
|
var price = _data.Bars.Close.Values.ToArray();
|
|||
|
|
var volume = _data.Bars.Volume.Values.ToArray();
|
|||
|
|
var spanValues = new double[price.Length];
|
|||
|
|
Evwma.Batch(price, volume, spanValues, period);
|
|||
|
|
|
|||
|
|
ValidationHelper.VerifyData(streamingValues.ToArray(), spanValues, 0, 100, 1e-10);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[Fact]
|
|||
|
|
public void Evwma_Batch_Matches_Span()
|
|||
|
|
{
|
|||
|
|
int period = 20;
|
|||
|
|
|
|||
|
|
// Batch
|
|||
|
|
var batchResult = Evwma.Batch(_data.Bars, period);
|
|||
|
|
var batchValues = batchResult.Values.ToArray();
|
|||
|
|
|
|||
|
|
// Span
|
|||
|
|
var price = _data.Bars.Close.Values.ToArray();
|
|||
|
|
var volume = _data.Bars.Volume.Values.ToArray();
|
|||
|
|
var spanValues = new double[price.Length];
|
|||
|
|
Evwma.Batch(price, volume, spanValues, period);
|
|||
|
|
|
|||
|
|
// Batch and Span use identical code path, should match exactly
|
|||
|
|
ValidationHelper.VerifyData(batchValues, spanValues, 0, 100, 1e-12);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ============ Known-Value Validation ============
|
|||
|
|
|
|||
|
|
[Fact]
|
|||
|
|
public void Evwma_KnownValues_ManualCalculation()
|
|||
|
|
{
|
|||
|
|
// Manually compute EVWMA for a small series
|
|||
|
|
// Period = 3
|
|||
|
|
// Bar 0: price=100, vol=10 → sumVol=10, result=100 (first bar)
|
|||
|
|
// Bar 1: price=110, vol=20 → sumVol=30, remain=10, result=(10*100+20*110)/30=3200/30≈106.6667
|
|||
|
|
// Bar 2: price=105, vol=15 → sumVol=45, remain=30, result=(30*106.6667+15*105)/45=4725/45=105
|
|||
|
|
// Wait: (30 × 106.6667 + 15 × 105) / 45 = (3200 + 1575) / 45 = 4775/45 ≈ 106.1111
|
|||
|
|
// Bar 3: price=120, vol=25 → drop bar0 vol(10): sumVol=45-10+25=60, remain=35
|
|||
|
|
// result = (35 * 106.1111 + 25 * 120) / 60 = (3713.889 + 3000) / 60 = 6713.889/60 ≈ 111.898
|
|||
|
|
|
|||
|
|
var evwma = new Evwma(3);
|
|||
|
|
|
|||
|
|
var bar0 = new TBar(DateTime.UtcNow, 100, 100, 100, 100, 10);
|
|||
|
|
var r0 = evwma.Update(bar0);
|
|||
|
|
Assert.Equal(100.0, r0.Value, 6);
|
|||
|
|
|
|||
|
|
var bar1 = new TBar(DateTime.UtcNow.AddMinutes(1), 110, 110, 110, 110, 20);
|
|||
|
|
var r1 = evwma.Update(bar1);
|
|||
|
|
// sumVol = 10 + 20 = 30; remainVol = 30 - 20 = 10
|
|||
|
|
// result = (10 * 100 + 20 * 110) / 30 = 3200 / 30
|
|||
|
|
Assert.Equal(3200.0 / 30.0, r1.Value, 10);
|
|||
|
|
|
|||
|
|
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(2), 105, 105, 105, 105, 15);
|
|||
|
|
var r2 = evwma.Update(bar2);
|
|||
|
|
// sumVol = 10 + 20 + 15 = 45; remainVol = 45 - 15 = 30
|
|||
|
|
// prevResult = 3200/30
|
|||
|
|
// result = (30 * (3200/30) + 15 * 105) / 45 = (3200 + 1575) / 45 = 4775 / 45
|
|||
|
|
Assert.Equal(4775.0 / 45.0, r2.Value, 10);
|
|||
|
|
|
|||
|
|
var bar3 = new TBar(DateTime.UtcNow.AddMinutes(3), 120, 120, 120, 120, 25);
|
|||
|
|
var r3 = evwma.Update(bar3);
|
|||
|
|
// Bar0 vol drops: sumVol = (10+20+15) - 10 + 25 = 60; remainVol = 60 - 25 = 35
|
|||
|
|
// prevResult = 4775/45
|
|||
|
|
// result = (35 * (4775/45) + 25 * 120) / 60
|
|||
|
|
double prev = 4775.0 / 45.0;
|
|||
|
|
double expected = (35.0 * prev + 25.0 * 120.0) / 60.0;
|
|||
|
|
Assert.Equal(expected, r3.Value, 10);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[Fact]
|
|||
|
|
public void Evwma_DifferentPeriods_ProduceDifferentResults()
|
|||
|
|
{
|
|||
|
|
int period1 = 5;
|
|||
|
|
int period2 = 50;
|
|||
|
|
|
|||
|
|
var result1 = Evwma.Batch(_data.Bars, period1);
|
|||
|
|
var result2 = Evwma.Batch(_data.Bars, period2);
|
|||
|
|
|
|||
|
|
// At later bars, different periods should produce different values
|
|||
|
|
int idx = 100;
|
|||
|
|
Assert.NotEqual(result1.Values[idx], result2.Values[idx], 6);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[Fact]
|
|||
|
|
public void Evwma_ConstantPrice_ReturnsConstant()
|
|||
|
|
{
|
|||
|
|
// If all prices are the same, EVWMA should always return that price
|
|||
|
|
// regardless of volume
|
|||
|
|
int period = 10;
|
|||
|
|
var bars = new TBarSeries();
|
|||
|
|
var now = DateTime.UtcNow;
|
|||
|
|
for (int i = 0; i < 50; i++)
|
|||
|
|
{
|
|||
|
|
bars.Add(new TBar(now.AddMinutes(i), 42.0, 42.0, 42.0, 42.0, 100 + i * 10));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var result = Evwma.Batch(bars, period);
|
|||
|
|
|
|||
|
|
for (int i = 0; i < 50; i++)
|
|||
|
|
{
|
|||
|
|
Assert.Equal(42.0, result.Values[i], 10);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[Fact]
|
|||
|
|
public void Evwma_UniformVolume_BehavesLikeRunningAverage()
|
|||
|
|
{
|
|||
|
|
// With uniform volume=1 and period covering all bars,
|
|||
|
|
// EVWMA degenerates to a specific recursive average
|
|||
|
|
int period = 100;
|
|||
|
|
var evwma = new Evwma(period);
|
|||
|
|
|
|||
|
|
double[] prices = [100, 110, 105, 120, 95, 115, 108, 112, 103, 118];
|
|||
|
|
|
|||
|
|
for (int i = 0; i < prices.Length; i++)
|
|||
|
|
{
|
|||
|
|
var tv = new TValue(DateTime.UtcNow.AddMinutes(i), prices[i]);
|
|||
|
|
evwma.Update(tv);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Result should be finite and within the price range
|
|||
|
|
Assert.True(double.IsFinite(evwma.Last.Value));
|
|||
|
|
Assert.True(evwma.Last.Value >= 90 && evwma.Last.Value <= 130,
|
|||
|
|
$"EVWMA value {evwma.Last.Value} should be within price range");
|
|||
|
|
}
|
|||
|
|
}
|