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

686 lines
22 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.
using Xunit;
namespace QuanTAlib.Tests;
public class GammadistTests
{
private const double Tolerance = 1e-10;
// ─── A) Constructor validation ────────────────────────────────────────────
[Fact]
public void Constructor_DefaultParameters_SetsProperties()
{
var indicator = new Gammadist();
Assert.Equal("Gammadist(2.00,1.00,14)", indicator.Name);
Assert.Equal(14, indicator.WarmupPeriod);
Assert.False(indicator.IsHot);
}
[Fact]
public void Constructor_CustomParameters_SetsName()
{
var indicator = new Gammadist(alpha: 3.0, beta: 2.0, period: 20);
Assert.Equal("Gammadist(3.00,2.00,20)", indicator.Name);
Assert.Equal(20, indicator.WarmupPeriod);
}
[Fact]
public void Constructor_ZeroAlpha_ThrowsArgumentException()
{
var ex = Assert.Throws<ArgumentException>(() => new Gammadist(alpha: 0.0));
Assert.Equal("alpha", ex.ParamName);
}
[Fact]
public void Constructor_NegativeAlpha_ThrowsArgumentException()
{
var ex = Assert.Throws<ArgumentException>(() => new Gammadist(alpha: -1.0));
Assert.Equal("alpha", ex.ParamName);
}
[Fact]
public void Constructor_ZeroBeta_ThrowsArgumentException()
{
var ex = Assert.Throws<ArgumentException>(() => new Gammadist(beta: 0.0));
Assert.Equal("beta", ex.ParamName);
}
[Fact]
public void Constructor_NegativeBeta_ThrowsArgumentException()
{
var ex = Assert.Throws<ArgumentException>(() => new Gammadist(beta: -0.5));
Assert.Equal("beta", ex.ParamName);
}
[Fact]
public void Constructor_PeriodOne_ThrowsArgumentException()
{
var ex = Assert.Throws<ArgumentException>(() => new Gammadist(period: 1));
Assert.Equal("period", ex.ParamName);
}
[Fact]
public void Constructor_NegativePeriod_ThrowsArgumentException()
{
var ex = Assert.Throws<ArgumentException>(() => new Gammadist(period: -1));
Assert.Equal("period", ex.ParamName);
}
[Fact]
public void Constructor_ZeroPeriod_ThrowsArgumentException()
{
var ex = Assert.Throws<ArgumentException>(() => new Gammadist(period: 0));
Assert.Equal("period", ex.ParamName);
}
// ─── B) Basic calculation ─────────────────────────────────────────────────
[Fact]
public void Update_ReturnsValidTValue()
{
var indicator = new Gammadist(period: 5);
var time = DateTime.UtcNow;
var input = new TValue(time, 100.0);
var result = indicator.Update(input);
Assert.Equal(input.Time, result.Time);
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Update_OutputInRange()
{
var indicator = new Gammadist(alpha: 2.0, beta: 1.0, period: 5);
var time = DateTime.UtcNow;
double[] prices = { 100.0, 102.0, 98.0, 105.0, 103.0 };
foreach (var p in prices)
{
indicator.Update(new TValue(time, p));
time = time.AddMinutes(1);
}
Assert.True(indicator.Last.Value >= 0.0, "Output must be >= 0");
Assert.True(indicator.Last.Value <= 1.0, "Output must be <= 1");
}
[Fact]
public void Last_IsAccessible_AfterUpdate()
{
var indicator = new Gammadist(period: 3);
var time = DateTime.UtcNow;
indicator.Update(new TValue(time, 50.0));
Assert.NotEqual(default, indicator.Last);
}
[Fact]
public void Name_ContainsGammadist()
{
var indicator = new Gammadist(alpha: 2.0, beta: 1.0, period: 14);
Assert.Contains("Gammadist", indicator.Name, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void IsHot_Property_ReflectsWarmup()
{
var indicator = new Gammadist(period: 5);
var time = DateTime.UtcNow;
for (int i = 0; i < 4; i++)
{
indicator.Update(new TValue(time.AddMinutes(i), 100.0 + i));
Assert.False(indicator.IsHot);
}
indicator.Update(new TValue(time.AddMinutes(4), 104.0));
Assert.True(indicator.IsHot);
}
[Fact]
public void Update_AtMaxOfWindow_ReturnsNearOne()
{
var indicator = new Gammadist(alpha: 2.0, beta: 1.0, period: 5);
var time = DateTime.UtcNow;
double[] prices = { 100.0, 102.0, 98.0, 101.0, 110.0 }; // 110 is max
foreach (var p in prices)
{
indicator.Update(new TValue(time, p));
time = time.AddMinutes(1);
}
// When x=1.0 → xGamma=10.0, Gamma CDF well above 0.9 for α=2,β=1
Assert.True(indicator.Last.Value > 0.9, $"Expected near 1 but got {indicator.Last.Value}");
}
[Fact]
public void Update_AtMinOfWindow_ReturnsZero()
{
var indicator = new Gammadist(alpha: 2.0, beta: 1.0, period: 5);
var time = DateTime.UtcNow;
double[] prices = { 110.0, 102.0, 98.0, 101.0, 90.0 }; // 90 is min
foreach (var p in prices)
{
indicator.Update(new TValue(time, p));
time = time.AddMinutes(1);
}
Assert.Equal(0.0, indicator.Last.Value, Tolerance);
}
// ─── C) State + bar correction ────────────────────────────────────────────
[Fact]
public void Update_IsNewTrue_AdvancesState()
{
var indicator = new Gammadist(period: 5);
var time = DateTime.UtcNow;
double[] prices = { 100.0, 102.0, 98.0, 105.0, 103.0 };
foreach (var p in prices)
{
indicator.Update(new TValue(time, p));
time = time.AddMinutes(1);
}
double first = indicator.Last.Value;
indicator.Update(new TValue(time, 110.0));
double second = indicator.Last.Value;
Assert.NotEqual(first, second, Tolerance);
}
[Fact]
public void Update_IsNewFalse_RewritesLastBar()
{
var indicator = new Gammadist(period: 5);
var time = DateTime.UtcNow;
double[] prices = { 100.0, 102.0, 98.0, 105.0, 103.0 };
foreach (var p in prices)
{
indicator.Update(new TValue(time, p));
time = time.AddMinutes(1);
}
// New bar with value A
indicator.Update(new TValue(time, 110.0), true);
double valueA = indicator.Last.Value;
// Correct same bar with value B
indicator.Update(new TValue(time, 90.0), false);
double valueB = indicator.Last.Value;
Assert.NotEqual(valueA, valueB, Tolerance);
}
[Fact]
public void Update_IterativeCorrection_RestoresState()
{
var time = DateTime.UtcNow;
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 72001);
var bars = gbm.Fetch(20, time.Ticks, TimeSpan.FromMinutes(1));
// Streaming without corrections
var straight = new Gammadist(period: 5);
for (int i = 0; i < bars.Close.Count; i++)
{
straight.Update(bars.Close[i]);
}
double finalStraight = straight.Last.Value;
// With corrections (wrong → corrected)
var corrected = new Gammadist(period: 5);
for (int i = 0; i < bars.Close.Count; i++)
{
corrected.Update(new TValue(bars.Close[i].Time, 999.0), true);
corrected.Update(bars.Close[i], false);
}
Assert.Equal(finalStraight, corrected.Last.Value, Tolerance);
}
[Fact]
public void Reset_ClearsState()
{
var indicator = new Gammadist(period: 5);
var time = DateTime.UtcNow;
double[] prices = { 100.0, 102.0, 98.0, 105.0, 103.0 };
foreach (var p in prices)
{
indicator.Update(new TValue(time, p));
time = time.AddMinutes(1);
}
Assert.True(indicator.IsHot);
indicator.Reset();
Assert.False(indicator.IsHot);
Assert.Equal(default, indicator.Last);
}
// ─── D) Warmup / convergence ──────────────────────────────────────────────
[Fact]
public void IsHot_FlipsAtPeriod()
{
int period = 10;
var indicator = new Gammadist(period: period);
var time = DateTime.UtcNow;
for (int i = 0; i < period - 1; i++)
{
indicator.Update(new TValue(time.AddMinutes(i), 100.0 + i));
Assert.False(indicator.IsHot, $"Should not be hot at bar {i + 1}");
}
indicator.Update(new TValue(time.AddMinutes(period - 1), 100.0 + period));
Assert.True(indicator.IsHot, "Should be hot after period bars");
}
[Fact]
public void WarmupPeriod_EqualsPeriod()
{
var indicator = new Gammadist(period: 25);
Assert.Equal(25, indicator.WarmupPeriod);
}
// ─── E) Robustness ────────────────────────────────────────────────────────
[Fact]
public void Update_NaN_UsesLastValidValue()
{
var indicator = new Gammadist(period: 5);
var time = DateTime.UtcNow;
double[] prices = { 100.0, 102.0, 98.0, 105.0, 103.0 };
foreach (var p in prices)
{
indicator.Update(new TValue(time, p));
time = time.AddMinutes(1);
}
double before = indicator.Last.Value;
indicator.Update(new TValue(time, double.NaN));
Assert.Equal(before, indicator.Last.Value, Tolerance);
}
[Fact]
public void Update_PositiveInfinity_UsesLastValidValue()
{
var indicator = new Gammadist(period: 5);
var time = DateTime.UtcNow;
double[] prices = { 100.0, 102.0, 98.0, 105.0, 103.0 };
foreach (var p in prices)
{
indicator.Update(new TValue(time, p));
time = time.AddMinutes(1);
}
double before = indicator.Last.Value;
indicator.Update(new TValue(time, double.PositiveInfinity));
Assert.Equal(before, indicator.Last.Value, Tolerance);
}
[Fact]
public void Update_NegativeInfinity_UsesLastValidValue()
{
var indicator = new Gammadist(period: 5);
var time = DateTime.UtcNow;
double[] prices = { 100.0, 102.0, 98.0, 105.0, 103.0 };
foreach (var p in prices)
{
indicator.Update(new TValue(time, p));
time = time.AddMinutes(1);
}
double before = indicator.Last.Value;
indicator.Update(new TValue(time, double.NegativeInfinity));
Assert.Equal(before, indicator.Last.Value, Tolerance);
}
[Fact]
public void Update_BatchNaN_Stable()
{
var indicator = new Gammadist(period: 5);
var time = DateTime.UtcNow;
double[] prices = { 100.0, double.NaN, 102.0, double.NaN, 98.0, 105.0, 103.0 };
foreach (var p in prices)
{
var result = indicator.Update(new TValue(time, p));
Assert.True(double.IsFinite(result.Value), "Output must always be finite");
time = time.AddMinutes(1);
}
}
[Fact]
public void Update_FlatRange_ReturnsCdfAtFive()
{
// When all values in window are identical, range=0 → xNorm=0.5 → xGamma=5.0
double alpha = 2.0;
double beta = 1.0;
var indicator = new Gammadist(alpha: alpha, beta: beta, period: 5);
var time = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
indicator.Update(new TValue(time.AddMinutes(i), 100.0));
}
double expected = Gammadist.GammaCdf(5.0, alpha, beta);
Assert.Equal(expected, indicator.Last.Value, 1e-6);
}
// ─── F) Consistency: batch == streaming == span == eventing ──────────────
[Fact]
public void AllModes_ConsistencyCheck()
{
int count = 100;
int period = 20;
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 72002);
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var source = bars.Close;
// Streaming
var streaming = new Gammadist(period: period);
for (int i = 0; i < source.Count; i++)
{
streaming.Update(source[i]);
}
// Batch (TSeries)
var batch = Gammadist.Batch(source, period: period);
// Span
var rawValues = new double[source.Count];
for (int i = 0; i < source.Count; i++)
{
rawValues[i] = source[i].Value;
}
var spanOutput = new double[source.Count];
Gammadist.Batch(rawValues, spanOutput, period: period);
// Eventing
var eventResults = new List<double>();
var eventSource = new TSeries();
var eventIndicator = new Gammadist(eventSource, period: period);
eventIndicator.Pub += (object? s, in TValueEventArgs e) => eventResults.Add(e.Value.Value);
for (int i = 0; i < source.Count; i++)
{
eventSource.Add(source[i], true);
}
// Verify last value matches across all modes
double streamingLast = streaming.Last.Value;
double batchLast = batch[source.Count - 1].Value;
double spanLast = spanOutput[source.Count - 1];
double eventLast = eventResults[^1];
Assert.Equal(streamingLast, batchLast, Tolerance);
Assert.Equal(streamingLast, spanLast, Tolerance);
Assert.Equal(streamingLast, eventLast, Tolerance);
}
[Fact]
public void Streaming_VsBatch_AllValues_Match()
{
int count = 80;
int period = 15;
var gbm = new GBM(startPrice: 50, mu: 0.0, sigma: 0.3, seed: 72003);
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var source = bars.Close;
var streaming = new Gammadist(period: period);
var streamingVals = new double[count];
for (int i = 0; i < count; i++)
{
streaming.Update(source[i]);
streamingVals[i] = streaming.Last.Value;
}
var batch = Gammadist.Batch(source, period: period);
for (int i = 0; i < count; i++)
{
Assert.Equal(streamingVals[i], batch[i].Value, Tolerance);
}
}
// ─── G) Span API tests ────────────────────────────────────────────────────
[Fact]
public void Batch_Span_EmptySource_ThrowsArgumentException()
{
var ex = Assert.Throws<ArgumentException>(() =>
Gammadist.Batch([], Array.Empty<double>()));
Assert.Equal("source", ex.ParamName);
}
[Fact]
public void Batch_Span_OutputTooShort_ThrowsArgumentException()
{
double[] src = { 1.0, 2.0, 3.0 };
double[] dst = new double[2];
var ex = Assert.Throws<ArgumentException>(() =>
Gammadist.Batch(src, dst));
Assert.Equal("output", ex.ParamName);
}
[Fact]
public void Batch_Span_ZeroAlpha_ThrowsArgumentException()
{
double[] src = { 1.0, 2.0, 3.0 };
double[] dst = new double[3];
var ex = Assert.Throws<ArgumentException>(() =>
Gammadist.Batch(src, dst, alpha: 0.0));
Assert.Equal("alpha", ex.ParamName);
}
[Fact]
public void Batch_Span_ZeroBeta_ThrowsArgumentException()
{
double[] src = { 1.0, 2.0, 3.0 };
double[] dst = new double[3];
var ex = Assert.Throws<ArgumentException>(() =>
Gammadist.Batch(src, dst, beta: 0.0));
Assert.Equal("beta", ex.ParamName);
}
[Fact]
public void Batch_Span_InvalidPeriod_ThrowsArgumentException()
{
double[] src = { 1.0, 2.0, 3.0 };
double[] dst = new double[3];
var ex = Assert.Throws<ArgumentException>(() =>
Gammadist.Batch(src, dst, period: 1));
Assert.Equal("period", ex.ParamName);
}
[Fact]
public void Batch_Span_OutputInRange()
{
int count = 100;
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 72004);
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
double[] src = new double[count];
for (int i = 0; i < count; i++)
{
src[i] = bars.Close[i].Value;
}
double[] dst = new double[count];
Gammadist.Batch(src, dst, period: 20);
foreach (double v in dst)
{
Assert.True(v >= 0.0 && v <= 1.0, $"Output {v} out of [0,1] range");
}
}
[Fact]
public void Batch_Span_HandlesNaN()
{
double[] src = { 100.0, double.NaN, 102.0, 98.0, 105.0, 103.0 };
double[] dst = new double[src.Length];
Gammadist.Batch(src, dst, period: 5);
foreach (double v in dst)
{
Assert.True(double.IsFinite(v), "Span output should always be finite");
}
}
[Fact]
public void Batch_Span_NoStackOverflow_LargeData()
{
int count = 5000;
double[] src = new double[count];
for (int i = 0; i < count; i++)
{
src[i] = 100.0 + Math.Sin(i * 0.1) * 10.0;
}
double[] dst = new double[count];
Gammadist.Batch(src, dst, period: 300);
foreach (double v in dst)
{
Assert.True(double.IsFinite(v));
}
}
[Fact]
public void Batch_Span_MatchesStreaming()
{
int count = 60;
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.25, seed: 72005);
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
double[] src = new double[count];
for (int i = 0; i < count; i++)
{
src[i] = bars.Close[i].Value;
}
double[] spanOut = new double[count];
Gammadist.Batch(src, spanOut, period: 14);
var streaming = new Gammadist(period: 14);
for (int i = 0; i < count; i++)
{
streaming.Update(bars.Close[i]);
Assert.Equal(streaming.Last.Value, spanOut[i], Tolerance);
}
}
// ─── H) Chainability ──────────────────────────────────────────────────────
[Fact]
public void Pub_EventFires()
{
var indicator = new Gammadist(period: 3);
int count = 0;
indicator.Pub += (object? sender, in TValueEventArgs args) => count++;
var time = DateTime.UtcNow;
indicator.Update(new TValue(time, 100.0));
indicator.Update(new TValue(time.AddMinutes(1), 102.0));
indicator.Update(new TValue(time.AddMinutes(2), 98.0));
Assert.Equal(3, count);
}
[Fact]
public void Chaining_Constructor_Works()
{
int period = 5;
var source = new TSeries();
var indicator = new Gammadist(source, period: period);
var time = DateTime.UtcNow;
double[] prices = { 100.0, 102.0, 98.0, 105.0, 103.0 };
foreach (var p in prices)
{
source.Add(new TValue(time, p), true);
time = time.AddMinutes(1);
}
Assert.True(indicator.IsHot);
Assert.True(indicator.Last.Value >= 0.0 && indicator.Last.Value <= 1.0);
}
[Fact]
public void Pub_EventValue_MatchesLast()
{
var indicator = new Gammadist(period: 5);
TValue? lastEvent = null;
indicator.Pub += (object? s, in TValueEventArgs e) => lastEvent = e.Value;
var time = DateTime.UtcNow;
double[] prices = { 100.0, 102.0, 98.0, 105.0, 103.0 };
foreach (var p in prices)
{
indicator.Update(new TValue(time, p));
time = time.AddMinutes(1);
}
Assert.NotNull(lastEvent);
Assert.Equal(indicator.Last.Value, lastEvent.Value.Value, Tolerance);
}
// ─── Additional: Alpha/Beta parameter effects ─────────────────────────────
[Fact]
public void DifferentAlpha_ProduceDifferentResults()
{
int count = 60;
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 72006);
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var ind1 = new Gammadist(alpha: 1.0, beta: 1.0, period: 20);
var ind2 = new Gammadist(alpha: 2.0, beta: 1.0, period: 20);
var ind3 = new Gammadist(alpha: 5.0, beta: 1.0, period: 20);
for (int i = 0; i < count; i++)
{
ind1.Update(bars.Close[i]);
ind2.Update(bars.Close[i]);
ind3.Update(bars.Close[i]);
}
// All outputs must be in [0, 1]
Assert.True(ind1.Last.Value >= 0.0 && ind1.Last.Value <= 1.0);
Assert.True(ind2.Last.Value >= 0.0 && ind2.Last.Value <= 1.0);
Assert.True(ind3.Last.Value >= 0.0 && ind3.Last.Value <= 1.0);
}
[Fact]
public void Calculate_StaticMethod_ReturnsTuple()
{
int count = 50;
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 72007);
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var (results, instance) = Gammadist.Calculate(bars.Close, period: 20);
Assert.Equal(count, results.Count);
Assert.True(instance.IsHot);
Assert.Equal(results[^1].Value, instance.Last.Value, Tolerance);
}
}