Files
Miha Kralj 060649192f 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
2026-03-12 12:34:16 -07:00

496 lines
16 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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");
}
}