mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-02 11:37:42 +00:00
09ffd31a40
- Updated class count in classes.svg from 938 to 1078. - Adjusted comments percentage in comments.svg from 33.06 to 33.02. - Revised average cyclomatic complexity in complexity.svg from 2.19 to 2.12. - Increased source files count in files.svg from 1099 to 1275. - Updated lines of code in loc.svg from 114549 to 129859. - Increased methods count in methods.svg from 12035 to 14066. - Updated public types count in public-api.svg from 1086 to 1225. - Revised missing indicators report with updated counts and categories, reflecting recent implementations and planned additions.
183 lines
5.4 KiB
C#
183 lines
5.4 KiB
C#
// HURST Validation Tests - Hurst Exponent via Rescaled Range (R/S) Analysis
|
|
// Validated against self-consistency and known mathematical properties
|
|
// No external library provides a direct R/S-based Hurst exponent equivalent
|
|
|
|
namespace QuanTAlib.Tests;
|
|
|
|
public sealed class HurstValidationTests
|
|
{
|
|
private static TSeries CreateGbmSeries(int count = 500, double mu = 0.0, double sigma = 0.2, int seed = 42)
|
|
{
|
|
var gbm = new GBM(startPrice: 100.0, mu: mu, sigma: sigma, seed: seed);
|
|
var times = new List<long>(count);
|
|
var values = new List<double>(count);
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
var bar = gbm.Next(isNew: true);
|
|
times.Add(bar.Time);
|
|
values.Add(bar.Close);
|
|
}
|
|
return new TSeries(times, values);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A pure random walk (GBM with zero drift) should produce H near 0.5.
|
|
/// </summary>
|
|
[Fact]
|
|
public void RandomWalk_HurstNearHalf()
|
|
{
|
|
const int period = 100;
|
|
var series = CreateGbmSeries(count: 1000, mu: 0.0, sigma: 0.2, seed: 42);
|
|
|
|
var h = new Hurst(period);
|
|
for (int i = 0; i < series.Count; i++)
|
|
{
|
|
h.Update(series[i]);
|
|
}
|
|
|
|
// H should be approximately 0.5 for random walk — allow generous tolerance
|
|
Assert.InRange(h.Last.Value, 0.25, 0.75);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Multiple independent random walks should all produce H near 0.5.
|
|
/// </summary>
|
|
[Fact]
|
|
public void MultipleRandomWalks_AllNearHalf()
|
|
{
|
|
const int period = 100;
|
|
int[] seeds = [42, 123, 456, 789, 1024];
|
|
|
|
foreach (int seed in seeds)
|
|
{
|
|
var series = CreateGbmSeries(count: 500, mu: 0.0, sigma: 0.2, seed: seed);
|
|
|
|
var h = new Hurst(period);
|
|
for (int i = 0; i < series.Count; i++)
|
|
{
|
|
h.Update(series[i]);
|
|
}
|
|
|
|
Assert.InRange(h.Last.Value, 0.2, 0.8);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Hurst exponent range — should always produce finite values within theoretically meaningful bounds.
|
|
/// </summary>
|
|
[Fact]
|
|
public void HurstRange_AlwaysFinite()
|
|
{
|
|
const int period = 50;
|
|
var series = CreateGbmSeries(count: 300, mu: 0.05, sigma: 0.2, seed: 42);
|
|
var h = new Hurst(period);
|
|
|
|
for (int i = 0; i < series.Count; i++)
|
|
{
|
|
var result = h.Update(series[i]);
|
|
Assert.True(double.IsFinite(result.Value), $"Value at {i} is not finite: {result.Value}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Batch and streaming must produce identical results.
|
|
/// </summary>
|
|
[Fact]
|
|
public void BatchVsStreaming_ExactMatch()
|
|
{
|
|
const int period = 20;
|
|
var series = CreateGbmSeries(count: 200, mu: 0.05, sigma: 0.2, seed: 42);
|
|
|
|
// Batch
|
|
var batchResult = Hurst.Batch(series, period);
|
|
|
|
// Streaming
|
|
var streamingInd = new Hurst(period);
|
|
for (int i = 0; i < series.Count; i++)
|
|
{
|
|
streamingInd.Update(series[i]);
|
|
}
|
|
|
|
Assert.Equal(batchResult.Last.Value, streamingInd.Last.Value, 1e-12);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Span batch must match TSeries batch exactly.
|
|
/// </summary>
|
|
[Fact]
|
|
public void SpanBatch_MatchesTSeriesBatch()
|
|
{
|
|
const int period = 30;
|
|
var series = CreateGbmSeries(count: 200, mu: 0.05, sigma: 0.2, seed: 42);
|
|
|
|
var tseriesResult = Hurst.Batch(series, period);
|
|
|
|
double[] source = new double[series.Count];
|
|
double[] output = new double[series.Count];
|
|
for (int i = 0; i < series.Count; i++)
|
|
{
|
|
source[i] = series[i].Value;
|
|
}
|
|
|
|
Hurst.Batch(source.AsSpan(), output.AsSpan(), period);
|
|
|
|
for (int i = 0; i < series.Count; i++)
|
|
{
|
|
Assert.Equal(tseriesResult[i].Value, output[i], 1e-10);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constant price series should produce H = 0.5 (degenerate — all log returns = 0).
|
|
/// </summary>
|
|
[Fact]
|
|
public void ConstantSeries_ReturnsDefaultHalf()
|
|
{
|
|
const int period = 20;
|
|
var h = new Hurst(period);
|
|
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
h.Update(new TValue(DateTime.UtcNow, 100.0));
|
|
}
|
|
|
|
// All log returns are zero → stddev = 0 → no valid R/S → default 0.5
|
|
Assert.Equal(0.5, h.Last.Value, 1e-10);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculate static method returns both results and indicator.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Calculate_ReturnsResultsAndIndicator()
|
|
{
|
|
var series = CreateGbmSeries(count: 100, mu: 0.05, sigma: 0.2, seed: 42);
|
|
var (results, indicator) = Hurst.Calculate(series, 20);
|
|
|
|
Assert.Equal(series.Count, results.Count);
|
|
Assert.True(indicator.IsHot);
|
|
Assert.Equal(results.Last.Value, indicator.Last.Value, 1e-12);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deterministic: same input always produces identical output.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Deterministic_SameInputSameOutput()
|
|
{
|
|
const int period = 30;
|
|
var series = CreateGbmSeries(count: 200, mu: 0.05, sigma: 0.2, seed: 42);
|
|
|
|
var h1 = new Hurst(period);
|
|
var h2 = new Hurst(period);
|
|
|
|
for (int i = 0; i < series.Count; i++)
|
|
{
|
|
h1.Update(series[i]);
|
|
h2.Update(series[i]);
|
|
}
|
|
|
|
Assert.Equal(h1.Last.Value, h2.Last.Value, 1e-15);
|
|
}
|
|
}
|