mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-22 12:38:06 +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,311 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class EwmaIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void EwmaIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new EwmaIndicator();
|
||||
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.True(indicator.AnnualizeVol);
|
||||
Assert.Equal(252, indicator.AnnualPeriods);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("EWMA - Exponentially Weighted Moving Average Volatility", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EwmaIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new EwmaIndicator { Period = 14, AnnualizeVol = true, AnnualPeriods = 252 };
|
||||
Assert.Contains("EWMA", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("14", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EwmaIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new EwmaIndicator();
|
||||
|
||||
Assert.Equal(0, EwmaIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EwmaIndicator_Initialize_CreatesInternalEwma()
|
||||
{
|
||||
var indicator = new EwmaIndicator();
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EwmaIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new EwmaIndicator { Period = 5, AnnualizeVol = false };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data with varying prices
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double basePrice = 100 + i + (i % 5); // Varying prices
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 2, basePrice - 2, basePrice + 1, 1000);
|
||||
|
||||
// Process update for each bar to simulate history loading
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Line series should have a value
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EwmaIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new EwmaIndicator { Period = 5, AnnualizeVol = false };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 2, 1000);
|
||||
}
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Add new bar with price change
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(30), 120, 125, 115, 122, 1500);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EwmaIndicator_DifferentPeriods_Work()
|
||||
{
|
||||
int[] periods = { 5, 10, 14, 20 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var indicator = new EwmaIndicator { Period = period, AnnualizeVol = false };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double basePrice = 100 + i + (i % 4);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 3, basePrice - 3, basePrice + 1, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val), $"Period {period} should produce finite value");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EwmaIndicator_DifferentAnnualPeriods_Work()
|
||||
{
|
||||
int[] annualPeriods = { 12, 52, 252, 365 };
|
||||
|
||||
foreach (var annualPeriod in annualPeriods)
|
||||
{
|
||||
var indicator = new EwmaIndicator { Period = 10, AnnualizeVol = true, AnnualPeriods = annualPeriod };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double basePrice = 100 + i + (i % 4);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 3, basePrice - 3, basePrice + 1, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val), $"Annual period {annualPeriod} should produce finite value");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EwmaIndicator_Period_CanBeChanged()
|
||||
{
|
||||
var indicator = new EwmaIndicator();
|
||||
Assert.Equal(20, indicator.Period);
|
||||
|
||||
indicator.Period = 14;
|
||||
Assert.Equal(14, indicator.Period);
|
||||
|
||||
indicator.Period = 30;
|
||||
Assert.Equal(30, indicator.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EwmaIndicator_AnnualizeVol_CanBeToggled()
|
||||
{
|
||||
var indicator = new EwmaIndicator();
|
||||
Assert.True(indicator.AnnualizeVol);
|
||||
|
||||
indicator.AnnualizeVol = false;
|
||||
Assert.False(indicator.AnnualizeVol);
|
||||
|
||||
indicator.AnnualizeVol = true;
|
||||
Assert.True(indicator.AnnualizeVol);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EwmaIndicator_AnnualPeriods_CanBeChanged()
|
||||
{
|
||||
var indicator = new EwmaIndicator();
|
||||
Assert.Equal(252, indicator.AnnualPeriods);
|
||||
|
||||
indicator.AnnualPeriods = 52;
|
||||
Assert.Equal(52, indicator.AnnualPeriods);
|
||||
|
||||
indicator.AnnualPeriods = 365;
|
||||
Assert.Equal(365, indicator.AnnualPeriods);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EwmaIndicator_ShowColdValues_CanBeToggled()
|
||||
{
|
||||
var indicator = new EwmaIndicator();
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = true;
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EwmaIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new EwmaIndicator();
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Ewma.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EwmaIndicator_ConstantPrices_ProducesZeroVolatility()
|
||||
{
|
||||
var indicator = new EwmaIndicator { Period = 5, AnnualizeVol = false };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 100, 100, 100, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val), "Constant prices should produce finite value");
|
||||
Assert.Equal(0.0, val, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EwmaIndicator_VolatilePrices_ProducesPositiveVolatility()
|
||||
{
|
||||
var indicator = new EwmaIndicator { Period = 5, AnnualizeVol = false };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
// Alternating prices to create volatility
|
||||
double price = (i % 2 == 0) ? 100 : 110;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 2, price - 2, price, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val), "Volatile prices should produce finite value");
|
||||
Assert.True(val > 0, "Volatile prices should produce positive volatility");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EwmaIndicator_AnnualizationMultipliesVolatility()
|
||||
{
|
||||
var indicatorNoAnn = new EwmaIndicator { Period = 10, AnnualizeVol = false };
|
||||
var indicatorAnn = new EwmaIndicator { Period = 10, AnnualizeVol = true, AnnualPeriods = 252 };
|
||||
indicatorNoAnn.Initialize();
|
||||
indicatorAnn.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double price = 100 + i + (i % 5);
|
||||
indicatorNoAnn.HistoricalData.AddBar(now.AddMinutes(i), price, price + 2, price - 2, price, 1000);
|
||||
indicatorAnn.HistoricalData.AddBar(now.AddMinutes(i), price, price + 2, price - 2, price, 1000);
|
||||
indicatorNoAnn.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicatorAnn.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double valNoAnn = indicatorNoAnn.LinesSeries[0].GetValue(0);
|
||||
double valAnn = indicatorAnn.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(valNoAnn));
|
||||
Assert.True(double.IsFinite(valAnn));
|
||||
|
||||
// Annualized should be approximately sqrt(252) times larger
|
||||
if (valNoAnn > 1e-10)
|
||||
{
|
||||
double ratio = valAnn / valNoAnn;
|
||||
double expectedRatio = Math.Sqrt(252);
|
||||
Assert.True(Math.Abs(ratio - expectedRatio) < 0.01,
|
||||
$"Annualized volatility ratio should be ~{expectedRatio}, got {ratio}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EwmaIndicator_ShorterPeriod_MoreResponsive()
|
||||
{
|
||||
var indicatorShort = new EwmaIndicator { Period = 5, AnnualizeVol = false };
|
||||
var indicatorLong = new EwmaIndicator { Period = 50, AnnualizeVol = false };
|
||||
indicatorShort.Initialize();
|
||||
indicatorLong.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Build up history with low volatility
|
||||
for (int i = 0; i < 60; i++)
|
||||
{
|
||||
indicatorShort.HistoricalData.AddBar(now.AddMinutes(i), 100, 101, 99, 100, 1000);
|
||||
indicatorLong.HistoricalData.AddBar(now.AddMinutes(i), 100, 101, 99, 100, 1000);
|
||||
indicatorShort.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicatorLong.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double shortBefore = indicatorShort.LinesSeries[0].GetValue(0);
|
||||
double longBefore = indicatorLong.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Inject shock
|
||||
indicatorShort.HistoricalData.AddBar(now.AddMinutes(60), 100, 120, 80, 110, 1500);
|
||||
indicatorLong.HistoricalData.AddBar(now.AddMinutes(60), 100, 120, 80, 110, 1500);
|
||||
indicatorShort.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
indicatorLong.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
double shortAfter = indicatorShort.LinesSeries[0].GetValue(0);
|
||||
double longAfter = indicatorLong.LinesSeries[0].GetValue(0);
|
||||
|
||||
double shortIncrease = shortAfter - shortBefore;
|
||||
double longIncrease = longAfter - longBefore;
|
||||
|
||||
Assert.True(shortIncrease > longIncrease,
|
||||
"Shorter period should respond more strongly to shocks");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,553 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
using Xunit;
|
||||
|
||||
public class EwmaTests
|
||||
{
|
||||
private static TBarSeries GenerateTestData(int count = 100)
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidatesInput()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Ewma(0));
|
||||
Assert.Throws<ArgumentException>(() => new Ewma(-1));
|
||||
Assert.Throws<ArgumentException>(() => new Ewma(20, annualize: true, annualPeriods: 0));
|
||||
Assert.Throws<ArgumentException>(() => new Ewma(20, annualize: true, annualPeriods: -1));
|
||||
|
||||
var valid = new Ewma(10, true, 252);
|
||||
Assert.Equal(10, valid.Period);
|
||||
Assert.True(valid.Annualize);
|
||||
Assert.Equal(252, valid.AnnualPeriods);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_IsCorrect()
|
||||
{
|
||||
var ewma = new Ewma(20);
|
||||
Assert.Equal(20, ewma.WarmupPeriod);
|
||||
Assert.True(ewma.WarmupPeriod > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Properties_Accessible()
|
||||
{
|
||||
var ewma = new Ewma(20, true, 252);
|
||||
Assert.Equal(20, ewma.Period);
|
||||
Assert.True(ewma.Annualize);
|
||||
Assert.Equal(252, ewma.AnnualPeriods);
|
||||
Assert.Equal("Ewma(20,252)", ewma.Name);
|
||||
|
||||
var ewmaNoAnn = new Ewma(15, false);
|
||||
Assert.Equal("Ewma(15)", ewmaNoAnn.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BasicCalculation_DoesNotCrash()
|
||||
{
|
||||
var ewma = new Ewma(5);
|
||||
var bars = GenerateTestData(100);
|
||||
var times = bars.Times;
|
||||
var close = bars.CloseValues;
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = ewma.Update(new TValue(times[i], close[i]));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_ReturnsValue()
|
||||
{
|
||||
var ewma = new Ewma(10);
|
||||
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
var result = ewma.Update(new TValue(DateTime.UtcNow, 100 + i));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
Assert.True(ewma.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_IsNew_AcceptsParameter()
|
||||
{
|
||||
var ewma = new Ewma(10);
|
||||
|
||||
var result1 = ewma.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
|
||||
var result2 = ewma.Update(new TValue(DateTime.UtcNow, 101), isNew: true);
|
||||
var result3 = ewma.Update(new TValue(DateTime.UtcNow, 102), isNew: false);
|
||||
|
||||
Assert.True(double.IsFinite(result1.Value));
|
||||
Assert.True(double.IsFinite(result2.Value));
|
||||
Assert.True(double.IsFinite(result3.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_IsNew_False_UpdatesValue()
|
||||
{
|
||||
var ewma = new Ewma(5);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
ewma.Update(new TValue(DateTime.UtcNow, 100 + i), isNew: true);
|
||||
}
|
||||
|
||||
var baseline = ewma.Update(new TValue(DateTime.UtcNow, 110), isNew: true);
|
||||
var updated = ewma.Update(new TValue(DateTime.UtcNow, 150), isNew: false);
|
||||
|
||||
Assert.NotEqual(baseline.Value, updated.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BecomesTrueAfterWarmup()
|
||||
{
|
||||
int period = 10;
|
||||
var ewma = new Ewma(period);
|
||||
|
||||
for (int i = 0; i < period - 1; i++)
|
||||
{
|
||||
ewma.Update(new TValue(DateTime.UtcNow, 100 + i));
|
||||
Assert.False(ewma.IsHot);
|
||||
}
|
||||
|
||||
ewma.Update(new TValue(DateTime.UtcNow, 110));
|
||||
Assert.True(ewma.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_Works()
|
||||
{
|
||||
var ewma = new Ewma(10);
|
||||
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
ewma.Update(new TValue(DateTime.UtcNow, 100 + i));
|
||||
}
|
||||
Assert.True(ewma.IsHot);
|
||||
|
||||
ewma.Reset();
|
||||
Assert.False(ewma.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SingleValue_ReturnsZeroVolatility()
|
||||
{
|
||||
var ewma = new Ewma(5);
|
||||
var result = ewma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
// First value should return 0 (no return to calculate)
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.True(result.Value >= 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrections_ChangesValue()
|
||||
{
|
||||
var ewma = new Ewma(20);
|
||||
var bars = GenerateTestData(50);
|
||||
var times = bars.Times;
|
||||
var close = bars.CloseValues;
|
||||
|
||||
TValue lastValue = default;
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
lastValue = ewma.Update(new TValue(times[i], close[i]), isNew: true);
|
||||
}
|
||||
double originalValue = lastValue.Value;
|
||||
|
||||
// Verify that isNew=false with different price produces different output
|
||||
var correctedValue = ewma.Update(new TValue(DateTime.UtcNow, 999.99), isNew: false);
|
||||
Assert.NotEqual(originalValue, correctedValue.Value);
|
||||
|
||||
// Verify output is still finite and positive
|
||||
Assert.True(double.IsFinite(correctedValue.Value));
|
||||
Assert.True(correctedValue.Value >= 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNew_Consistency()
|
||||
{
|
||||
var ewma = new Ewma(10);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
ewma.Update(new TValue(DateTime.UtcNow, 100 + i), isNew: true);
|
||||
}
|
||||
|
||||
var result1 = ewma.Update(new TValue(DateTime.UtcNow, 110), isNew: true);
|
||||
_ = ewma.Update(new TValue(DateTime.UtcNow, 115), isNew: false);
|
||||
var result3 = ewma.Update(new TValue(DateTime.UtcNow, 110), isNew: false);
|
||||
|
||||
// With same input, should get same output after rollback
|
||||
Assert.Equal(result1.Value, result3.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NaN_Input_UsesLastValidValue()
|
||||
{
|
||||
var ewma = new Ewma(5);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
ewma.Update(new TValue(DateTime.UtcNow, 100 + i));
|
||||
}
|
||||
|
||||
var resultNan = ewma.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
Assert.True(double.IsFinite(resultNan.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_Input_UsesLastValidValue()
|
||||
{
|
||||
var ewma = new Ewma(5);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
ewma.Update(new TValue(DateTime.UtcNow, 100 + i));
|
||||
}
|
||||
|
||||
var resultInf = ewma.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(resultInf.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LargeDataset_Performance()
|
||||
{
|
||||
var ewma = new Ewma(50);
|
||||
var bars = GenerateTestData(5000);
|
||||
var times = bars.Times;
|
||||
var close = bars.CloseValues;
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = ewma.Update(new TValue(times[i], close[i]));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TSeries_Update_MatchesStreaming()
|
||||
{
|
||||
int period = 20;
|
||||
var ewmaStream = new Ewma(period);
|
||||
var ewmaBatch = new Ewma(period);
|
||||
var bars = GenerateTestData(100);
|
||||
var times = bars.Times;
|
||||
var close = bars.CloseValues;
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
ewmaStream.Update(new TValue(times[i], close[i]));
|
||||
}
|
||||
|
||||
var ts = new TSeries();
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
ts.Add(new TValue(times[i], close[i]));
|
||||
}
|
||||
var result = ewmaBatch.Update(ts);
|
||||
|
||||
Assert.Equal(ewmaStream.Last.Value, result[result.Count - 1].Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchCalc_MatchesIterativeCalc()
|
||||
{
|
||||
var ewma = new Ewma(20);
|
||||
var bars = GenerateTestData(200);
|
||||
var times = bars.Times;
|
||||
var close = bars.CloseValues;
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
ewma.Update(new TValue(times[i], close[i]));
|
||||
}
|
||||
var iterativeResult = ewma.Last.Value;
|
||||
|
||||
var ts = new TSeries();
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
ts.Add(new TValue(times[i], close[i]));
|
||||
}
|
||||
var batchResult = Ewma.Batch(ts, 20);
|
||||
|
||||
Assert.Equal(iterativeResult, batchResult[batchResult.Count - 1].Value, 1e-8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaticBatch_Works()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
var times = bars.Times;
|
||||
var close = bars.CloseValues;
|
||||
|
||||
var ts = new TSeries();
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
ts.Add(new TValue(times[i], close[i]));
|
||||
}
|
||||
|
||||
var result = Ewma.Batch(ts, 20);
|
||||
|
||||
Assert.Equal(100, result.Count);
|
||||
Assert.True(double.IsFinite(result[result.Count - 1].Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaticBatch_ValidatesInput()
|
||||
{
|
||||
var ts = new TSeries();
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
ts.Add(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i));
|
||||
}
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Ewma.Batch(ts, 0));
|
||||
Assert.Throws<ArgumentException>(() => Ewma.Batch(ts, -1));
|
||||
Assert.Throws<ArgumentException>(() => Ewma.Batch(ts, 5, true, 0));
|
||||
Assert.Throws<ArgumentException>(() => Ewma.Batch(ts, 5, true, -1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_NaN_Safe()
|
||||
{
|
||||
var values = new double[] { 100, 101, 102, double.NaN, 104, 105 };
|
||||
var output = new double[values.Length];
|
||||
|
||||
Ewma.Batch(values, output, 3);
|
||||
|
||||
Assert.True(output.Length == 6);
|
||||
for (int i = 0; i < output.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstantPrices_ZeroVolatility()
|
||||
{
|
||||
var ewma = new Ewma(10, false); // Not annualized
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
ewma.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0));
|
||||
}
|
||||
|
||||
// Constant prices should have zero volatility (log returns = 0)
|
||||
Assert.True(ewma.Last.Value < 1e-10, "Constant prices should have near-zero volatility");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HighVolatility_ProducesHigherValue()
|
||||
{
|
||||
var ewmaStable = new Ewma(10, false);
|
||||
var ewmaVolatile = new Ewma(10, false);
|
||||
|
||||
// Stable prices (small changes)
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
ewmaStable.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i * 0.01));
|
||||
}
|
||||
|
||||
// Volatile prices (alternating)
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double volatilePrice = 100 + (i % 2 == 0 ? 5 : -5);
|
||||
ewmaVolatile.Update(new TValue(DateTime.UtcNow.AddMinutes(i), volatilePrice));
|
||||
}
|
||||
|
||||
Assert.True(ewmaVolatile.Last.Value > ewmaStable.Last.Value,
|
||||
"Higher volatility should produce higher EWMA");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Annualization_ScalesCorrectly()
|
||||
{
|
||||
var ewmaNoAnn = new Ewma(10, false);
|
||||
var ewmaAnn252 = new Ewma(10, true, 252);
|
||||
|
||||
var bars = GenerateTestData(50);
|
||||
var times = bars.Times;
|
||||
var close = bars.CloseValues;
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
ewmaNoAnn.Update(new TValue(times[i], close[i]));
|
||||
ewmaAnn252.Update(new TValue(times[i], close[i]));
|
||||
}
|
||||
|
||||
double expectedRatio = Math.Sqrt(252);
|
||||
double actualRatio = ewmaAnn252.Last.Value / ewmaNoAnn.Last.Value;
|
||||
|
||||
Assert.True(Math.Abs(actualRatio - expectedRatio) < 0.01,
|
||||
$"Annualization should scale by sqrt(252). Expected ratio: {expectedRatio}, Actual: {actualRatio}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentAnnualPeriods_ProduceDistinctValues()
|
||||
{
|
||||
var ewma252 = new Ewma(10, true, 252); // Daily
|
||||
var ewma52 = new Ewma(10, true, 52); // Weekly
|
||||
var ewma12 = new Ewma(10, true, 12); // Monthly
|
||||
|
||||
var bars = GenerateTestData(50);
|
||||
var times = bars.Times;
|
||||
var close = bars.CloseValues;
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
ewma252.Update(new TValue(times[i], close[i]));
|
||||
ewma52.Update(new TValue(times[i], close[i]));
|
||||
ewma12.Update(new TValue(times[i], close[i]));
|
||||
}
|
||||
|
||||
// Higher annual periods = higher annualized volatility
|
||||
Assert.True(ewma252.Last.Value > ewma52.Last.Value, "Daily annualization should be higher than weekly");
|
||||
Assert.True(ewma52.Last.Value > ewma12.Last.Value, "Weekly annualization should be higher than monthly");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BiasCorrection_WorksForEarlyValues()
|
||||
{
|
||||
// EWMA with bias correction should provide reasonable estimates even early
|
||||
var ewma = new Ewma(20, false);
|
||||
|
||||
// First few values
|
||||
ewma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
var first = ewma.Update(new TValue(DateTime.UtcNow.AddMinutes(1), 101));
|
||||
var second = ewma.Update(new TValue(DateTime.UtcNow.AddMinutes(2), 99));
|
||||
|
||||
// Should produce finite values even before warmup
|
||||
Assert.True(double.IsFinite(first.Value));
|
||||
Assert.True(double.IsFinite(second.Value));
|
||||
Assert.True(second.Value > 0, "Should detect volatility after price changes");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Chainability_Works()
|
||||
{
|
||||
var ewma = new Ewma(20);
|
||||
var sma = new Sma(5);
|
||||
var bars = GenerateTestData(100);
|
||||
var times = bars.Times;
|
||||
var close = bars.CloseValues;
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var ewmaResult = ewma.Update(new TValue(times[i], close[i]));
|
||||
sma.Update(ewmaResult);
|
||||
}
|
||||
|
||||
Assert.True(sma.IsHot);
|
||||
Assert.True(double.IsFinite(sma.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_ValidatesLengths()
|
||||
{
|
||||
var source = new double[] { 100, 101, 102, 103, 104 };
|
||||
var outputShort = new double[3];
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Ewma.Batch(source, outputShort, 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_ValidatesPeriod()
|
||||
{
|
||||
var source = new double[] { 100, 101, 102, 103, 104 };
|
||||
var output = new double[5];
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Ewma.Batch(source, output, 0));
|
||||
Assert.Throws<ArgumentException>(() => Ewma.Batch(source, output, -1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_ValidatesAnnualPeriods()
|
||||
{
|
||||
var source = new double[] { 100, 101, 102, 103, 104 };
|
||||
var output = new double[5];
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Ewma.Batch(source, output, 3, true, 0));
|
||||
Assert.Throws<ArgumentException>(() => Ewma.Batch(source, output, 3, true, -1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_MatchesStreaming()
|
||||
{
|
||||
var ewma = new Ewma(10, true, 252);
|
||||
var bars = GenerateTestData(100);
|
||||
var close = bars.CloseValues;
|
||||
|
||||
// Streaming
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
ewma.Update(new TValue(DateTime.UtcNow, close[i]));
|
||||
}
|
||||
|
||||
// Batch
|
||||
var output = new double[close.Length];
|
||||
Ewma.Batch(close, output, 10, true, 252);
|
||||
|
||||
// Compare last values
|
||||
Assert.Equal(ewma.Last.Value, output[output.Length - 1], 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyInput_HandledGracefully()
|
||||
{
|
||||
var source = ReadOnlySpan<double>.Empty;
|
||||
var output = Span<double>.Empty;
|
||||
|
||||
// Should not throw - empty spans are valid
|
||||
Ewma.Batch(source, output, 10);
|
||||
Assert.True(true, "Empty input handled without exception");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LogReturns_CalculatedCorrectly()
|
||||
{
|
||||
// Test with known values to verify log return calculation
|
||||
var ewma = new Ewma(2, false); // Short period for quick testing
|
||||
|
||||
// Price goes from 100 to 110 (+10%)
|
||||
ewma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
var result = ewma.Update(new TValue(DateTime.UtcNow.AddMinutes(1), 110));
|
||||
|
||||
// Log return = ln(110/100) ≈ 0.0953
|
||||
// Squared return ≈ 0.00908
|
||||
// With bias correction, volatility should be close to |log return|
|
||||
Assert.True(result.Value > 0);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NegativePrice_UsesLastValid()
|
||||
{
|
||||
var ewma = new Ewma(5);
|
||||
|
||||
ewma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
ewma.Update(new TValue(DateTime.UtcNow.AddMinutes(1), 101));
|
||||
var resultNeg = ewma.Update(new TValue(DateTime.UtcNow.AddMinutes(2), -50));
|
||||
|
||||
Assert.True(double.IsFinite(resultNeg.Value));
|
||||
Assert.True(resultNeg.Value >= 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZeroPrice_UsesLastValid()
|
||||
{
|
||||
var ewma = new Ewma(5);
|
||||
|
||||
ewma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
ewma.Update(new TValue(DateTime.UtcNow.AddMinutes(1), 101));
|
||||
var resultZero = ewma.Update(new TValue(DateTime.UtcNow.AddMinutes(2), 0));
|
||||
|
||||
Assert.True(double.IsFinite(resultZero.Value));
|
||||
Assert.True(resultZero.Value >= 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,495 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
using Xunit;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for EWMA Volatility indicator.
|
||||
/// Note: EWMA Volatility as implemented is based on PineScript reference.
|
||||
/// External library validation may not be available.
|
||||
/// </summary>
|
||||
public class EwmaValidationTests
|
||||
{
|
||||
private readonly int DefaultPeriod = 20;
|
||||
private readonly bool DefaultAnnualize = true;
|
||||
private readonly int DefaultAnnualPeriods = 252;
|
||||
private const double StreamingTolerance = 1e-9;
|
||||
|
||||
private static TBarSeries GenerateTestData(int count = 500)
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
private static TSeries ToTSeries(TBarSeries bars)
|
||||
{
|
||||
var ts = new TSeries();
|
||||
var times = bars.Times;
|
||||
var close = bars.CloseValues;
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
ts.Add(new TValue(times[i], close[i]));
|
||||
}
|
||||
return ts;
|
||||
}
|
||||
|
||||
// ============ Mathematical Property Validation ============
|
||||
|
||||
[Fact]
|
||||
public void MathProperty_ReturnsAreSquared()
|
||||
{
|
||||
// EWMA should always produce non-negative values (sqrt of squared returns)
|
||||
var ewma = new Ewma(10, false);
|
||||
var bars = GenerateTestData(100);
|
||||
var close = bars.CloseValues;
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = ewma.Update(new TValue(DateTime.UtcNow, close[i]));
|
||||
Assert.True(result.Value >= 0, $"EWMA should be non-negative, got {result.Value} at index {i}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MathProperty_AnnualizationFactor()
|
||||
{
|
||||
// Annualized vol = periodic vol × √(annual periods)
|
||||
var ewmaNoAnn = new Ewma(DefaultPeriod, false);
|
||||
var ewmaAnn252 = new Ewma(DefaultPeriod, true, 252);
|
||||
var ewmaAnn52 = new Ewma(DefaultPeriod, true, 52);
|
||||
var ewmaAnn12 = new Ewma(DefaultPeriod, true, 12);
|
||||
|
||||
var bars = GenerateTestData(100);
|
||||
var close = bars.CloseValues;
|
||||
var times = bars.Times;
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
ewmaNoAnn.Update(new TValue(times[i], close[i]));
|
||||
ewmaAnn252.Update(new TValue(times[i], close[i]));
|
||||
ewmaAnn52.Update(new TValue(times[i], close[i]));
|
||||
ewmaAnn12.Update(new TValue(times[i], close[i]));
|
||||
}
|
||||
|
||||
double periodicVol = ewmaNoAnn.Last.Value;
|
||||
if (periodicVol > 1e-10) // Only test if there's measurable volatility
|
||||
{
|
||||
Assert.Equal(periodicVol * Math.Sqrt(252), ewmaAnn252.Last.Value, 1e-9);
|
||||
Assert.Equal(periodicVol * Math.Sqrt(52), ewmaAnn52.Last.Value, 1e-9);
|
||||
Assert.Equal(periodicVol * Math.Sqrt(12), ewmaAnn12.Last.Value, 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MathProperty_BiasCorrection_ConvergesToOne()
|
||||
{
|
||||
// Bias correction factor (1 - decay^n) should approach 1 as n → ∞
|
||||
// This means corrected and uncorrected values should converge
|
||||
var ewma = new Ewma(20, false);
|
||||
var bars = GenerateTestData(500);
|
||||
var close = bars.CloseValues;
|
||||
var times = bars.Times;
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
ewma.Update(new TValue(times[i], close[i]));
|
||||
}
|
||||
|
||||
// After many observations, bias correction should be minimal
|
||||
// We can't directly test the factor, but we can verify stability
|
||||
Assert.True(ewma.IsHot);
|
||||
Assert.True(double.IsFinite(ewma.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MathProperty_RMA_ExponentialDecay()
|
||||
{
|
||||
// RMA formula: new_rma = (old_rma × (period-1) + new_value) / period
|
||||
// This is equivalent to EMA with alpha = 1/period
|
||||
// Older values should have exponentially decaying influence
|
||||
|
||||
var ewma = new Ewma(10, false);
|
||||
|
||||
// Feed constant values to establish baseline
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
ewma.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0));
|
||||
}
|
||||
double baselineVol = ewma.Last.Value;
|
||||
|
||||
// Inject a shock
|
||||
ewma.Update(new TValue(DateTime.UtcNow.AddMinutes(50), 150.0)); // 50% jump
|
||||
double shockVol = ewma.Last.Value;
|
||||
|
||||
Assert.True(shockVol > baselineVol, "Shock should increase volatility");
|
||||
|
||||
// Return to constant prices - volatility should decay
|
||||
double[] vols = new double[30];
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
ewma.Update(new TValue(DateTime.UtcNow.AddMinutes(51 + i), 100.0));
|
||||
vols[i] = ewma.Last.Value;
|
||||
}
|
||||
|
||||
// Verify monotonic decay (or near-monotonic)
|
||||
int decayCount = 0;
|
||||
for (int i = 1; i < vols.Length; i++)
|
||||
{
|
||||
if (vols[i] <= vols[i - 1] + 1e-10) // Allow small floating point noise
|
||||
{
|
||||
decayCount++;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(decayCount >= 25, $"Volatility should decay over time, but only {decayCount}/29 periods showed decay");
|
||||
}
|
||||
|
||||
// ============ Mode Consistency Validation ============
|
||||
|
||||
[Fact]
|
||||
public void ModeConsistency_StreamingVsBatch()
|
||||
{
|
||||
var ewmaStream = new Ewma(DefaultPeriod, DefaultAnnualize, DefaultAnnualPeriods);
|
||||
var bars = GenerateTestData(200);
|
||||
var ts = ToTSeries(bars);
|
||||
var close = bars.CloseValues;
|
||||
var times = bars.Times;
|
||||
|
||||
// Streaming
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
ewmaStream.Update(new TValue(times[i], close[i]));
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batchResult = Ewma.Batch(ts, DefaultPeriod, DefaultAnnualize, DefaultAnnualPeriods);
|
||||
|
||||
Assert.Equal(ewmaStream.Last.Value, batchResult[batchResult.Count - 1].Value, StreamingTolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModeConsistency_StreamingVsSpan()
|
||||
{
|
||||
var ewmaStream = new Ewma(DefaultPeriod, DefaultAnnualize, DefaultAnnualPeriods);
|
||||
var bars = GenerateTestData(200);
|
||||
var close = bars.CloseValues;
|
||||
var times = bars.Times;
|
||||
|
||||
// Streaming
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
ewmaStream.Update(new TValue(times[i], close[i]));
|
||||
}
|
||||
|
||||
// Span
|
||||
var output = new double[close.Length];
|
||||
Ewma.Batch(close, output, DefaultPeriod, DefaultAnnualize, DefaultAnnualPeriods);
|
||||
|
||||
Assert.Equal(ewmaStream.Last.Value, output[output.Length - 1], StreamingTolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModeConsistency_TSeries_VsSpan()
|
||||
{
|
||||
var ewma = new Ewma(DefaultPeriod, DefaultAnnualize, DefaultAnnualPeriods);
|
||||
var bars = GenerateTestData(200);
|
||||
var ts = ToTSeries(bars);
|
||||
var close = bars.CloseValues;
|
||||
|
||||
// TSeries
|
||||
var tseriesResult = ewma.Update(ts);
|
||||
|
||||
// Span
|
||||
var output = new double[close.Length];
|
||||
Ewma.Batch(close, output, DefaultPeriod, DefaultAnnualize, DefaultAnnualPeriods);
|
||||
|
||||
Assert.Equal(tseriesResult[tseriesResult.Count - 1].Value, output[output.Length - 1], StreamingTolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModeConsistency_AllFourModes()
|
||||
{
|
||||
var bars = GenerateTestData(150);
|
||||
var ts = ToTSeries(bars);
|
||||
var close = bars.CloseValues;
|
||||
var times = bars.Times;
|
||||
|
||||
// Mode 1: Streaming
|
||||
var ewmaStream = new Ewma(DefaultPeriod, DefaultAnnualize, DefaultAnnualPeriods);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
ewmaStream.Update(new TValue(times[i], close[i]));
|
||||
}
|
||||
double streamingResult = ewmaStream.Last.Value;
|
||||
|
||||
// Mode 2: TSeries Update
|
||||
var ewmaTSeries = new Ewma(DefaultPeriod, DefaultAnnualize, DefaultAnnualPeriods);
|
||||
var tseriesResult = ewmaTSeries.Update(ts);
|
||||
double tseriesValue = tseriesResult[tseriesResult.Count - 1].Value;
|
||||
|
||||
// Mode 3: Static Calculate
|
||||
var batchResult = Ewma.Batch(ts, DefaultPeriod, DefaultAnnualize, DefaultAnnualPeriods);
|
||||
double batchValue = batchResult[batchResult.Count - 1].Value;
|
||||
|
||||
// Mode 4: Span Batch
|
||||
var output = new double[close.Length];
|
||||
Ewma.Batch(close, output, DefaultPeriod, DefaultAnnualize, DefaultAnnualPeriods);
|
||||
double spanValue = output[output.Length - 1];
|
||||
|
||||
// All four should match
|
||||
Assert.Equal(streamingResult, tseriesValue, StreamingTolerance);
|
||||
Assert.Equal(streamingResult, batchValue, StreamingTolerance);
|
||||
Assert.Equal(streamingResult, spanValue, StreamingTolerance);
|
||||
}
|
||||
|
||||
// ============ Edge Case Validation ============
|
||||
|
||||
[Fact]
|
||||
public void EdgeCase_SingleValue()
|
||||
{
|
||||
var ewma = new Ewma(5, false);
|
||||
var result = ewma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
// Single value should return 0 volatility (no return yet)
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.Equal(0.0, result.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EdgeCase_TwoValues()
|
||||
{
|
||||
var ewma = new Ewma(5, false);
|
||||
ewma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
var result = ewma.Update(new TValue(DateTime.UtcNow.AddMinutes(1), 110));
|
||||
|
||||
// With price change, should have positive volatility
|
||||
Assert.True(result.Value > 0, "Should detect volatility from price change");
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EdgeCase_AllNaN()
|
||||
{
|
||||
var ewma = new Ewma(5);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var result = ewma.Update(new TValue(DateTime.UtcNow.AddMinutes(i), double.NaN));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EdgeCase_MixedNaN()
|
||||
{
|
||||
var ewma = new Ewma(5);
|
||||
double[] prices = { 100, 101, double.NaN, 103, double.NaN, double.NaN, 106 };
|
||||
|
||||
foreach (double price in prices)
|
||||
{
|
||||
var result = ewma.Update(new TValue(DateTime.UtcNow, price));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EdgeCase_VerySmallPrices()
|
||||
{
|
||||
var ewma = new Ewma(5, false);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double price = 0.0001 + (i % 2) * 0.00001;
|
||||
var result = ewma.Update(new TValue(DateTime.UtcNow.AddMinutes(i), price));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.True(result.Value >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EdgeCase_VeryLargePrices()
|
||||
{
|
||||
var ewma = new Ewma(5, false);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double price = 1e10 + (i % 2) * 1e9;
|
||||
var result = ewma.Update(new TValue(DateTime.UtcNow.AddMinutes(i), price));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.True(result.Value >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EdgeCase_Period1()
|
||||
{
|
||||
var ewma = new Ewma(1, false);
|
||||
ewma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
var result = ewma.Update(new TValue(DateTime.UtcNow.AddMinutes(1), 110));
|
||||
|
||||
// Period 1 means volatility is just |log return|
|
||||
double expectedLogReturn = Math.Abs(Math.Log(110.0 / 100.0));
|
||||
Assert.True(Math.Abs(result.Value - expectedLogReturn) < 0.01,
|
||||
$"Period 1 EWMA should equal |log return|. Expected ~{expectedLogReturn}, got {result.Value}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EdgeCase_LargePeriod()
|
||||
{
|
||||
var ewma = new Ewma(500, false);
|
||||
var bars = GenerateTestData(600);
|
||||
var close = bars.CloseValues;
|
||||
var times = bars.Times;
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = ewma.Update(new TValue(times[i], close[i]));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
Assert.True(ewma.IsHot);
|
||||
}
|
||||
|
||||
// ============ Stability Validation ============
|
||||
|
||||
[Fact]
|
||||
public void Stability_LongRunningCalculation()
|
||||
{
|
||||
var ewma = new Ewma(20);
|
||||
var bars = GenerateTestData(5000);
|
||||
var close = bars.CloseValues;
|
||||
var times = bars.Times;
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = ewma.Update(new TValue(times[i], close[i]));
|
||||
Assert.True(double.IsFinite(result.Value), $"Non-finite value at index {i}");
|
||||
Assert.True(result.Value >= 0, $"Negative volatility at index {i}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stability_RepeatedReset()
|
||||
{
|
||||
var ewma = new Ewma(10);
|
||||
var bars = GenerateTestData(50);
|
||||
var close = bars.CloseValues;
|
||||
var times = bars.Times;
|
||||
|
||||
for (int reset = 0; reset < 5; reset++)
|
||||
{
|
||||
ewma.Reset();
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = ewma.Update(new TValue(times[i], close[i]));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stability_BarCorrection_MultipleUpdates()
|
||||
{
|
||||
var ewma = new Ewma(10);
|
||||
var bars = GenerateTestData(50);
|
||||
var close = bars.CloseValues;
|
||||
var times = bars.Times;
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
ewma.Update(new TValue(times[i], close[i]), isNew: true);
|
||||
}
|
||||
|
||||
// Multiple corrections
|
||||
for (int j = 0; j < 10; j++)
|
||||
{
|
||||
double correctedPrice = 100 + j * 5;
|
||||
var result = ewma.Update(new TValue(DateTime.UtcNow, correctedPrice), isNew: false);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.True(result.Value >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Known Value Validation ============
|
||||
|
||||
[Fact]
|
||||
public void KnownValue_ConstantPrice_ZeroVolatility()
|
||||
{
|
||||
var ewma = new Ewma(10, false);
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
ewma.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0));
|
||||
}
|
||||
|
||||
Assert.Equal(0.0, ewma.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KnownValue_SimpleReturn()
|
||||
{
|
||||
// Verify log return calculation
|
||||
// If price goes 100 → 101, log return = ln(101/100) ≈ 0.00995
|
||||
var ewma = new Ewma(2, false);
|
||||
|
||||
ewma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
var result = ewma.Update(new TValue(DateTime.UtcNow.AddMinutes(1), 101));
|
||||
|
||||
double expectedLogReturn = Math.Log(101.0 / 100.0);
|
||||
// With period=2, RMA of first squared return is just that return
|
||||
// With bias correction at n=1, correction factor = 1 - 0.5 = 0.5
|
||||
// First squared return initialized to sq_ret, then bias correction applied
|
||||
// Volatility = sqrt(corrected variance)
|
||||
|
||||
Assert.True(result.Value > 0, "Volatility should be positive for price change");
|
||||
Assert.True(result.Value < 0.05, "Volatility should be reasonable for 1% price change");
|
||||
Assert.True(double.IsFinite(expectedLogReturn), "Log return should be finite");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KnownValue_SymmetricReturns()
|
||||
{
|
||||
// Volatility should be same for +10% and -10% returns (squared)
|
||||
var ewmaUp = new Ewma(5, false);
|
||||
var ewmaDown = new Ewma(5, false);
|
||||
|
||||
ewmaUp.Update(new TValue(DateTime.UtcNow, 100));
|
||||
ewmaDown.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
ewmaUp.Update(new TValue(DateTime.UtcNow.AddMinutes(1), 110)); // +10%
|
||||
ewmaDown.Update(new TValue(DateTime.UtcNow.AddMinutes(1), 90)); // -10%
|
||||
|
||||
// Log returns: ln(1.1) ≈ 0.0953, ln(0.9) ≈ -0.1054
|
||||
// Squared returns are slightly different due to log asymmetry
|
||||
// But both should be positive volatility
|
||||
Assert.True(ewmaUp.Last.Value > 0);
|
||||
Assert.True(ewmaDown.Last.Value > 0);
|
||||
}
|
||||
|
||||
// ============ Parameter Sensitivity Validation ============
|
||||
|
||||
[Fact]
|
||||
public void ParameterSensitivity_ShorterPeriod_MoreResponsive()
|
||||
{
|
||||
var ewmaShort = new Ewma(5, false);
|
||||
var ewmaLong = new Ewma(50, false);
|
||||
|
||||
// Build up history with low volatility
|
||||
for (int i = 0; i < 60; i++)
|
||||
{
|
||||
ewmaShort.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0));
|
||||
ewmaLong.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0));
|
||||
}
|
||||
|
||||
double shortBefore = ewmaShort.Last.Value;
|
||||
double longBefore = ewmaLong.Last.Value;
|
||||
|
||||
// Inject shock
|
||||
ewmaShort.Update(new TValue(DateTime.UtcNow.AddMinutes(60), 120.0));
|
||||
ewmaLong.Update(new TValue(DateTime.UtcNow.AddMinutes(60), 120.0));
|
||||
|
||||
double shortAfter = ewmaShort.Last.Value;
|
||||
double longAfter = ewmaLong.Last.Value;
|
||||
|
||||
double shortIncrease = shortAfter - shortBefore;
|
||||
double longIncrease = longAfter - longBefore;
|
||||
|
||||
Assert.True(shortIncrease > longIncrease,
|
||||
"Shorter period should respond more strongly to shocks");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user