mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-12 23:58:04 +00:00
352 lines
9.9 KiB
C#
352 lines
9.9 KiB
C#
namespace QuanTAlib.Tests;
|
|
|
|
using Xunit;
|
|
|
|
public class SineTests
|
|
{
|
|
private const double Tolerance = 1e-9;
|
|
private readonly GBM _gbm;
|
|
|
|
public SineTests()
|
|
{
|
|
_gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
|
|
}
|
|
|
|
private TBarSeries GenerateBars(int count)
|
|
{
|
|
return _gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromDays(1));
|
|
}
|
|
|
|
[Fact]
|
|
public void Sine_ConstructorDefaults()
|
|
{
|
|
var sine = new Sine();
|
|
Assert.Equal("SINE", sine.Name);
|
|
Assert.Equal(40, sine.HpPeriod);
|
|
Assert.Equal(10, sine.SsfPeriod);
|
|
Assert.Equal(48, sine.WarmupPeriod); // max(40, 10) + 8
|
|
Assert.False(sine.IsHot);
|
|
}
|
|
|
|
[Fact]
|
|
public void Sine_ConstructorCustomParameters()
|
|
{
|
|
var sine = new Sine(hpPeriod: 20, ssfPeriod: 5);
|
|
Assert.Equal(20, sine.HpPeriod);
|
|
Assert.Equal(5, sine.SsfPeriod);
|
|
Assert.Equal(28, sine.WarmupPeriod); // max(20, 5) + 8
|
|
}
|
|
|
|
[Fact]
|
|
public void Sine_ConstructorValidation_ThrowsOnInvalidHpPeriod()
|
|
{
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => new Sine(hpPeriod: 0));
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => new Sine(hpPeriod: -1));
|
|
}
|
|
|
|
[Fact]
|
|
public void Sine_ConstructorValidation_ThrowsOnInvalidSsfPeriod()
|
|
{
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => new Sine(ssfPeriod: 0));
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => new Sine(ssfPeriod: -1));
|
|
}
|
|
|
|
[Fact]
|
|
public void Sine_Update_ReturnsValidRange()
|
|
{
|
|
var sine = new Sine();
|
|
var bars = GenerateBars(200);
|
|
|
|
foreach (var bar in bars)
|
|
{
|
|
var result = sine.Update(new TValue(bar.Time, bar.Close));
|
|
Assert.True(result.Value >= -1.0 && result.Value <= 1.0,
|
|
$"Sine value {result.Value} out of range [-1, 1]");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Sine_IsHot_AfterWarmup()
|
|
{
|
|
var sine = new Sine(hpPeriod: 20, ssfPeriod: 5);
|
|
var bars = GenerateBars(50);
|
|
|
|
for (int i = 0; i < bars.Count; i++)
|
|
{
|
|
sine.Update(new TValue(bars[i].Time, bars[i].Close));
|
|
|
|
if (i + 1 < sine.WarmupPeriod)
|
|
{
|
|
Assert.False(sine.IsHot, $"Should not be hot at index {i}");
|
|
}
|
|
else
|
|
{
|
|
Assert.True(sine.IsHot, $"Should be hot at index {i}");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Sine_IsNew_AdvancesState()
|
|
{
|
|
var sine = new Sine();
|
|
var input = new TValue(DateTime.UtcNow, 100.0);
|
|
|
|
var result1 = sine.Update(input, isNew: true);
|
|
var result2 = sine.Update(new TValue(DateTime.UtcNow.AddDays(1), 101.0), isNew: true);
|
|
|
|
// With isNew=true, each call should advance state
|
|
// Values might be the same early on, but state should advance
|
|
Assert.NotEqual(result1.Time, result2.Time);
|
|
}
|
|
|
|
[Fact]
|
|
public void Sine_IsNew_False_UpdatesCurrentBar()
|
|
{
|
|
var sine = new Sine();
|
|
var bars = GenerateBars(60);
|
|
|
|
// Process first 50 bars normally
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
sine.Update(new TValue(bars[i].Time, bars[i].Close), isNew: true);
|
|
}
|
|
|
|
// Get result at bar 50
|
|
var newBarResult = sine.Update(new TValue(bars[50].Time, bars[50].Close), isNew: true);
|
|
|
|
// Reset and replay to bar 49, then update bar 50 with different value
|
|
var sine2 = new Sine();
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
sine2.Update(new TValue(bars[i].Time, bars[i].Close), isNew: true);
|
|
}
|
|
|
|
// First update bar 50
|
|
sine2.Update(new TValue(bars[50].Time, bars[50].Close), isNew: true);
|
|
|
|
// Update same bar with different value (bar correction)
|
|
var correctedResult = sine2.Update(new TValue(bars[50].Time, bars[50].Close * 1.1), isNew: false);
|
|
|
|
// Results should differ due to different input
|
|
Assert.NotEqual(newBarResult.Value, correctedResult.Value);
|
|
}
|
|
|
|
[Fact]
|
|
public void Sine_Reset_ClearsState()
|
|
{
|
|
var sine = new Sine();
|
|
var bars = GenerateBars(100);
|
|
|
|
foreach (var bar in bars)
|
|
{
|
|
sine.Update(new TValue(bar.Time, bar.Close));
|
|
}
|
|
|
|
Assert.True(sine.IsHot);
|
|
|
|
sine.Reset();
|
|
|
|
Assert.False(sine.IsHot);
|
|
Assert.Equal(0, sine.Last.Value);
|
|
}
|
|
|
|
[Fact]
|
|
public void Sine_TSeries_Update()
|
|
{
|
|
var bars = GenerateBars(100);
|
|
var series = new TSeries(100);
|
|
|
|
foreach (var bar in bars)
|
|
{
|
|
series.Add(new TValue(bar.Time, bar.Close));
|
|
}
|
|
|
|
var sine = new Sine();
|
|
var result = sine.Update(series);
|
|
|
|
Assert.Equal(100, result.Count);
|
|
|
|
// Verify all values are in range
|
|
foreach (var val in result)
|
|
{
|
|
Assert.True(val.Value >= -1.0 && val.Value <= 1.0);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Sine_StaticCalculate_TSeries()
|
|
{
|
|
var bars = GenerateBars(100);
|
|
var series = new TSeries(100);
|
|
|
|
foreach (var bar in bars)
|
|
{
|
|
series.Add(new TValue(bar.Time, bar.Close));
|
|
}
|
|
|
|
var result = Sine.Batch(series);
|
|
|
|
Assert.Equal(100, result.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void Sine_StaticCalculate_WithCustomParams()
|
|
{
|
|
var bars = GenerateBars(100);
|
|
var series = new TSeries(100);
|
|
|
|
foreach (var bar in bars)
|
|
{
|
|
series.Add(new TValue(bar.Time, bar.Close));
|
|
}
|
|
|
|
var result = Sine.Batch(series, hpPeriod: 20, ssfPeriod: 5);
|
|
|
|
Assert.Equal(100, result.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void Sine_Chaining_Works()
|
|
{
|
|
var source = new Sma(10);
|
|
var sine = new Sine(source);
|
|
|
|
bool eventFired = false;
|
|
sine.Pub += (object? sender, in TValueEventArgs args) => eventFired = true;
|
|
|
|
var input = new TValue(DateTime.UtcNow, 100.0);
|
|
source.Update(input);
|
|
|
|
Assert.True(eventFired);
|
|
}
|
|
|
|
[Fact]
|
|
public void Sine_EmptyTSeries_ReturnsEmpty()
|
|
{
|
|
var sine = new Sine();
|
|
var empty = new TSeries();
|
|
var result = sine.Update(empty);
|
|
|
|
Assert.Empty(result);
|
|
}
|
|
|
|
[Fact]
|
|
public void Sine_Streaming_MatchesBatch()
|
|
{
|
|
var bars = GenerateBars(200);
|
|
var series = new TSeries(200);
|
|
|
|
foreach (var bar in bars)
|
|
{
|
|
series.Add(new TValue(bar.Time, bar.Close));
|
|
}
|
|
|
|
// Streaming calculation
|
|
var streamingSine = new Sine();
|
|
var streamingResults = new List<double>();
|
|
foreach (var bar in bars)
|
|
{
|
|
var result = streamingSine.Update(new TValue(bar.Time, bar.Close));
|
|
streamingResults.Add(result.Value);
|
|
}
|
|
|
|
// Batch calculation
|
|
var batchResult = Sine.Batch(series);
|
|
|
|
// Compare last 100 values (after warmup)
|
|
for (int i = 100; i < 200; i++)
|
|
{
|
|
Assert.Equal(streamingResults[i], batchResult[i].Value, Tolerance);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Sine_NaN_HandledGracefully()
|
|
{
|
|
var sine = new Sine();
|
|
var bars = GenerateBars(60);
|
|
|
|
// Process some bars
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
sine.Update(new TValue(bars[i].Time, bars[i].Close));
|
|
}
|
|
|
|
// Feed NaN - should substitute with last valid value
|
|
var nanResult = sine.Update(new TValue(DateTime.UtcNow, double.NaN));
|
|
|
|
// Result should NOT be NaN (last-valid substitution) and in valid range
|
|
Assert.False(double.IsNaN(nanResult.Value), "NaN should not propagate");
|
|
Assert.True(nanResult.Value >= -1.0 && nanResult.Value <= 1.0,
|
|
$"Value {nanResult.Value} should be in [-1, 1]");
|
|
}
|
|
|
|
[Fact]
|
|
public void Sine_Prime_InitializesState()
|
|
{
|
|
var bars = GenerateBars(100);
|
|
var primeData = bars.Select(b => b.Close).ToArray();
|
|
|
|
var sine = new Sine();
|
|
sine.Prime(primeData);
|
|
|
|
Assert.True(sine.IsHot);
|
|
}
|
|
|
|
[Fact]
|
|
public void Sine_WithCyclingData_ProducesOscillation()
|
|
{
|
|
var sine = new Sine(hpPeriod: 20, ssfPeriod: 5);
|
|
|
|
// Generate sinusoidal price data
|
|
var results = new List<double>();
|
|
var baseTime = DateTime.UtcNow;
|
|
|
|
for (int i = 0; i < 200; i++)
|
|
{
|
|
// Create a price with embedded 30-bar cycle
|
|
double price = 100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 30.0);
|
|
var result = sine.Update(new TValue(baseTime.AddDays(i), price));
|
|
results.Add(result.Value);
|
|
}
|
|
|
|
// After warmup, check that we have both positive and negative values
|
|
var afterWarmup = results.Skip(30).ToList();
|
|
Assert.True(afterWarmup.Any(v => v > 0.5), "Should have positive cycle values");
|
|
Assert.True(afterWarmup.Any(v => v < -0.5), "Should have negative cycle values");
|
|
}
|
|
|
|
[Fact]
|
|
public void Sine_ConstantInput_ProducesValidOutput()
|
|
{
|
|
var sine = new Sine();
|
|
var baseTime = DateTime.UtcNow;
|
|
|
|
// Feed constant values
|
|
for (int i = 0; i < 200; i++)
|
|
{
|
|
var result = sine.Update(new TValue(baseTime.AddDays(i), 100.0));
|
|
// Output should always be in valid range regardless of input
|
|
Assert.True(result.Value >= -1.0 && result.Value <= 1.0,
|
|
$"Value {result.Value} out of range at index {i}");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Sine_TrendingInput_ProducesValidOutput()
|
|
{
|
|
var sine = new Sine(hpPeriod: 40, ssfPeriod: 10);
|
|
var baseTime = DateTime.UtcNow;
|
|
|
|
// Feed trending data (very low frequency)
|
|
for (int i = 0; i < 200; i++)
|
|
{
|
|
double price = 100.0 + i * 0.1; // Slow uptrend
|
|
var result = sine.Update(new TValue(baseTime.AddDays(i), price));
|
|
// Output should always be in valid range regardless of input
|
|
Assert.True(result.Value >= -1.0 && result.Value <= 1.0,
|
|
$"Value {result.Value} out of range at index {i}");
|
|
}
|
|
}
|
|
} |