mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-23 13:08:04 +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,545 @@
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// A) Constructor Validation
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
public class HurstConstructorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_ThrowsOnPeriodLessThan20()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Hurst(19));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Hurst(10));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Hurst(0));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Hurst(-1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_AcceptsMinimumPeriod()
|
||||
{
|
||||
var h = new Hurst(20);
|
||||
Assert.NotNull(h);
|
||||
Assert.Equal("Hurst(20)", h.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SetsWarmupPeriod()
|
||||
{
|
||||
var h = new Hurst(100);
|
||||
Assert.Equal(101, h.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ParamName_IsPeriod()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new Hurst(5));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_LargePeriodAccepted()
|
||||
{
|
||||
var h = new Hurst(500);
|
||||
Assert.Equal("Hurst(500)", h.Name);
|
||||
Assert.Equal(501, h.WarmupPeriod);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// B) Basic Calculation
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
public class HurstBasicTests
|
||||
{
|
||||
[Fact]
|
||||
public void Calc_ReturnsValue()
|
||||
{
|
||||
var h = new Hurst(20);
|
||||
TValue result = h.Update(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.Equal(result.Value, h.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_FirstValue_ReturnsDefaultHalf()
|
||||
{
|
||||
var h = new Hurst(20);
|
||||
TValue result = h.Update(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.Equal(0.5, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_OutputIsFinite()
|
||||
{
|
||||
var h = new Hurst(20);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
var result = h.Update(new TValue(bar.Time, bar.Close));
|
||||
Assert.True(double.IsFinite(result.Value), $"Result at index {i} is not finite: {result.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_GBM_ResultNearHalf()
|
||||
{
|
||||
// GBM with zero drift should produce H ≈ 0.5 (random walk)
|
||||
var h = new Hurst(100);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.0, sigma: 0.2, seed: 42);
|
||||
|
||||
TValue lastResult = default;
|
||||
for (int i = 0; i < 500; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
lastResult = h.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
// H should be roughly 0.5 for random walk — allow wide tolerance
|
||||
Assert.InRange(lastResult.Value, 0.2, 0.8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_Accessible()
|
||||
{
|
||||
var h = new Hurst(20);
|
||||
Assert.False(h.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Name_IsAccessible()
|
||||
{
|
||||
var h = new Hurst(50);
|
||||
Assert.Equal("Hurst(50)", h.Name);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// C) State + Bar Correction
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
public class HurstStateCorrectionTests
|
||||
{
|
||||
[Fact]
|
||||
public void IsNew_True_Advances()
|
||||
{
|
||||
var h = new Hurst(20);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
h.Update(new TValue(bar.Time, bar.Close), isNew: true);
|
||||
}
|
||||
double v1 = h.Last.Value;
|
||||
|
||||
var nextBar = gbm.Next(isNew: true);
|
||||
h.Update(new TValue(nextBar.Time, nextBar.Close), isNew: true);
|
||||
double v2 = h.Last.Value;
|
||||
|
||||
// Values should differ after advancing
|
||||
Assert.True(double.IsFinite(v1));
|
||||
Assert.True(double.IsFinite(v2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNew_False_Rewrites()
|
||||
{
|
||||
var h = new Hurst(20);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
|
||||
// Build up state
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
h.Update(new TValue(bar.Time, bar.Close), isNew: true);
|
||||
}
|
||||
|
||||
_ = h.Last.Value;
|
||||
|
||||
// Rewrite last value
|
||||
h.Update(new TValue(DateTime.UtcNow, 200), isNew: false);
|
||||
double valueAfterRewrite = h.Last.Value;
|
||||
|
||||
// Rewrite again with original-like value
|
||||
h.Update(new TValue(DateTime.UtcNow, 200), isNew: false);
|
||||
double valueSecondRewrite = h.Last.Value;
|
||||
|
||||
// Same rewrite value should produce same result
|
||||
Assert.Equal(valueAfterRewrite, valueSecondRewrite, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var h = new Hurst(20);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
|
||||
// Feed 25 new values
|
||||
TValue lastInput = default;
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
lastInput = new TValue(bar.Time, bar.Close);
|
||||
h.Update(lastInput, isNew: true);
|
||||
}
|
||||
|
||||
double stateAfter25 = h.Last.Value;
|
||||
|
||||
// Generate corrections with isNew=false using same last price
|
||||
h.Update(new TValue(DateTime.UtcNow, lastInput.Value + 10), isNew: false);
|
||||
h.Update(new TValue(DateTime.UtcNow, lastInput.Value + 20), isNew: false);
|
||||
|
||||
// Restore original value
|
||||
TValue finalResult = h.Update(lastInput, isNew: false);
|
||||
|
||||
Assert.Equal(stateAfter25, finalResult.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var h = new Hurst(20);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
h.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
Assert.True(h.IsHot);
|
||||
|
||||
h.Reset();
|
||||
Assert.False(h.IsHot);
|
||||
Assert.Equal(0, h.Last.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// D) Warmup / Convergence
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
public class HurstWarmupTests
|
||||
{
|
||||
[Fact]
|
||||
public void IsHot_BecomesTrueWhenBufferFull()
|
||||
{
|
||||
var h = new Hurst(20);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
|
||||
// Need period+1 = 21 prices to get 20 log returns
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
h.Update(new TValue(bar.Time, bar.Close));
|
||||
Assert.False(h.IsHot, $"Should not be hot at input {i + 1}");
|
||||
}
|
||||
|
||||
// 21st price → 20th log return → buffer full
|
||||
var finalBar = gbm.Next(isNew: true);
|
||||
h.Update(new TValue(finalBar.Time, finalBar.Close));
|
||||
Assert.True(h.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_MatchesPeriodPlusOne()
|
||||
{
|
||||
var h = new Hurst(50);
|
||||
Assert.Equal(51, h.WarmupPeriod);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// E) Robustness
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
public class HurstRobustnessTests
|
||||
{
|
||||
[Fact]
|
||||
public void NaN_UsesLastValidValue()
|
||||
{
|
||||
var h = new Hurst(20);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
h.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
var result = h.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PositiveInfinity_UsesLastValidValue()
|
||||
{
|
||||
var h = new Hurst(20);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
h.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
var result = h.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NegativeInfinity_UsesLastValidValue()
|
||||
{
|
||||
var h = new Hurst(20);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
h.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
var result = h.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchNaN_Safe()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
double[] source = new double[50];
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
source[i] = gbm.Next(isNew: true).Close;
|
||||
}
|
||||
source[10] = double.NaN;
|
||||
source[25] = double.NaN;
|
||||
|
||||
double[] output = new double[source.Length];
|
||||
Hurst.Batch(source.AsSpan(), output.AsSpan(), 20);
|
||||
|
||||
for (int i = 0; i < output.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]), $"output[{i}] = {output[i]}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// F) Consistency (all modes match)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
public class HurstConsistencyTests
|
||||
{
|
||||
[Fact]
|
||||
public void AllModes_ProduceSameResult()
|
||||
{
|
||||
const int period = 20;
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
|
||||
int count = 100;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
var series = new TSeries(times, values);
|
||||
|
||||
// 1. Batch Mode (static method)
|
||||
var batchSeries = Hurst.Batch(series, period);
|
||||
double expected = batchSeries.Last.Value;
|
||||
|
||||
// 2. Span Mode (static method with spans)
|
||||
var spanInput = values.ToArray();
|
||||
var spanOutput = new double[count];
|
||||
Hurst.Batch(spanInput.AsSpan(), spanOutput.AsSpan(), period);
|
||||
double spanResult = spanOutput[^1];
|
||||
|
||||
// 3. Streaming Mode (instance, one value at a time)
|
||||
var streamingInd = new Hurst(period);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
streamingInd.Update(series[i]);
|
||||
}
|
||||
double streamingResult = streamingInd.Last.Value;
|
||||
|
||||
// Assert all modes produce identical results
|
||||
Assert.Equal(expected, spanResult, precision: 9);
|
||||
Assert.Equal(expected, streamingResult, precision: 9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Matches_Streaming()
|
||||
{
|
||||
const int period = 20;
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
int count = 60;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Streaming
|
||||
var h = new Hurst(period);
|
||||
var streamingResults = new List<double>();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
streamingResults.Add(h.Update(new TValue(times[i], values[i])).Value);
|
||||
}
|
||||
|
||||
// Batch
|
||||
var series = new TSeries(times, values);
|
||||
var batchResult = Hurst.Batch(series, period);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i], batchResult.Values[i], precision: 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// G) Span API Tests
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
public class HurstSpanTests
|
||||
{
|
||||
[Fact]
|
||||
public void SpanBatch_ValidatesLengths()
|
||||
{
|
||||
double[] source = new double[50];
|
||||
double[] wrongSize = new double[30];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Hurst.Batch(source.AsSpan(), wrongSize.AsSpan(), 20));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_ValidatesPeriod()
|
||||
{
|
||||
double[] source = new double[50];
|
||||
double[] output = new double[50];
|
||||
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
Hurst.Batch(source.AsSpan(), output.AsSpan(), 19));
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
Hurst.Batch(source.AsSpan(), output.AsSpan(), 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_MatchesTSeriesBatch()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
int count = 100;
|
||||
|
||||
var times = new List<long>(count);
|
||||
var values = new List<double>(count);
|
||||
double[] source = new double[count];
|
||||
double[] output = new double[count];
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
times.Add(bar.Time);
|
||||
values.Add(bar.Close);
|
||||
source[i] = bar.Close;
|
||||
}
|
||||
|
||||
var series = new TSeries(times, values);
|
||||
|
||||
var tseriesResult = Hurst.Batch(series, 20);
|
||||
Hurst.Batch(source.AsSpan(), output.AsSpan(), 20);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Assert.Equal(tseriesResult[i].Value, output[i], 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_HandlesNaN()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
double[] source = new double[50];
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
source[i] = gbm.Next(isNew: true).Close;
|
||||
}
|
||||
source[5] = double.NaN;
|
||||
source[15] = double.NaN;
|
||||
|
||||
double[] output = new double[50];
|
||||
Hurst.Batch(source.AsSpan(), output.AsSpan(), 20);
|
||||
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_LargeData_NoStackOverflow()
|
||||
{
|
||||
int count = 5000;
|
||||
var data = new double[count];
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
data[i] = gbm.Next(isNew: true).Close;
|
||||
}
|
||||
|
||||
var output = new double[count];
|
||||
|
||||
// Should not throw StackOverflowException
|
||||
Hurst.Batch(data.AsSpan(), output.AsSpan(), 100);
|
||||
|
||||
Assert.True(double.IsFinite(output[^1]));
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// H) Event / Chainability
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
public class HurstEventTests
|
||||
{
|
||||
[Fact]
|
||||
public void Pub_Fires()
|
||||
{
|
||||
var h = new Hurst(20);
|
||||
int eventCount = 0;
|
||||
h.Pub += (object? sender, in TValueEventArgs args) => eventCount++;
|
||||
|
||||
h.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
Assert.Equal(1, eventCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventBased_Chaining_Works()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var h = new Hurst(20);
|
||||
|
||||
source.Pub += (object? sender, in TValueEventArgs args) =>
|
||||
{
|
||||
h.Update(args.Value);
|
||||
};
|
||||
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
source.Add(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
Assert.True(h.IsHot);
|
||||
Assert.True(double.IsFinite(h.Last.Value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
|
||||
using Skender.Stock.Indicators;
|
||||
using QuanTAlib.Tests;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Structural comparison with Skender GetHurst — both compute Hurst exponent
|
||||
/// but may use different R/S subdivision strategies and regression methods.
|
||||
/// Validates that Skender produces finite results in the same range.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validate_Skender_Hurst_Structural()
|
||||
{
|
||||
const int period = 20;
|
||||
using var data = new ValidationTestData(10000);
|
||||
|
||||
// QuanTAlib streaming
|
||||
var indicator = new Hurst(period);
|
||||
foreach (var tv in data.Data)
|
||||
{
|
||||
indicator.Update(tv);
|
||||
}
|
||||
|
||||
// Skender
|
||||
var sResult = data.SkenderQuotes.GetHurst(period).ToList();
|
||||
|
||||
// QuanTAlib produces finite output
|
||||
Assert.True(double.IsFinite(indicator.Last.Value), "QuanTAlib Hurst last must be finite");
|
||||
|
||||
// Skender produces finite Hurst exponents
|
||||
int sFinite = sResult.Count(r => r.HurstExponent is not null && double.IsFinite(r.HurstExponent.Value));
|
||||
Assert.True(sFinite > 50, $"Skender produced only {sFinite} finite Hurst values");
|
||||
|
||||
// Both Hurst exponents should be finite
|
||||
foreach (var r in sResult.Where(r => r.HurstExponent is not null))
|
||||
{
|
||||
Assert.True(double.IsFinite(r.HurstExponent!.Value),
|
||||
$"Skender Hurst value {r.HurstExponent.Value} is not finite");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user