mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-13 16:18:05 +00:00
284 lines
8.6 KiB
C#
284 lines
8.6 KiB
C#
using System;
|
|
using Xunit;
|
|
using QuanTAlib;
|
|
|
|
namespace QuanTAlib.Tests;
|
|
|
|
public class GBMTests
|
|
{
|
|
[Fact]
|
|
public void Next_DefaultParameter_GeneratesNewBar()
|
|
{
|
|
var gbm = new GBM(startPrice: 100.0);
|
|
|
|
var bar1 = gbm.Next();
|
|
var bar2 = gbm.Next();
|
|
|
|
Assert.NotEqual(bar1.Time, bar2.Time);
|
|
Assert.True(bar2.Time > bar1.Time);
|
|
}
|
|
|
|
[Fact]
|
|
public void Next_IsNewTrue_AdvancesToNewBar()
|
|
{
|
|
var gbm = new GBM(startPrice: 100.0);
|
|
|
|
var bar1 = gbm.Next(isNew: true);
|
|
var bar2 = gbm.Next(isNew: true);
|
|
|
|
Assert.NotEqual(bar1.Time, bar2.Time);
|
|
Assert.True(bar2.Time > bar1.Time);
|
|
}
|
|
|
|
[Fact]
|
|
public void Next_IsNewFalse_UpdatesCurrentBar()
|
|
{
|
|
var gbm = new GBM(startPrice: 100.0);
|
|
|
|
var bar1 = gbm.Next(isNew: true);
|
|
long initialTime = bar1.Time;
|
|
|
|
var bar2 = gbm.Next(isNew: false);
|
|
|
|
Assert.Equal(initialTime, bar2.Time);
|
|
// Price likely changed (GBM random walk)
|
|
Assert.NotEqual(bar1.Close, bar2.Close);
|
|
}
|
|
|
|
[Fact]
|
|
public void Next_RefBool_HonorsRequest()
|
|
{
|
|
var gbm = new GBM(startPrice: 100.0);
|
|
|
|
// GBM always honors isNew - parameter should remain unchanged
|
|
bool isNew1 = true;
|
|
var bar1 = gbm.Next(ref isNew1);
|
|
Assert.True(isNew1, "GBM should honor isNew=true request");
|
|
|
|
bool isNew2 = false;
|
|
long time1 = bar1.Time;
|
|
var bar2 = gbm.Next(ref isNew2);
|
|
Assert.False(isNew2, "GBM should honor isNew=false request");
|
|
Assert.Equal(time1, bar2.Time);
|
|
|
|
bool isNew3 = true;
|
|
var bar3 = gbm.Next(ref isNew3);
|
|
Assert.True(isNew3, "GBM should honor isNew=true request");
|
|
Assert.NotEqual(time1, bar3.Time);
|
|
}
|
|
|
|
[Fact]
|
|
public void Fetch_GeneratesCorrectCount()
|
|
{
|
|
var gbm = new GBM(startPrice: 100.0);
|
|
int count = 10;
|
|
long startTime = DateTime.UtcNow.Ticks;
|
|
var interval = TimeSpan.FromMinutes(1);
|
|
|
|
var series = gbm.Fetch(count, startTime, interval);
|
|
|
|
Assert.Equal(count, series.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void Fetch_GeneratesSequentialBars()
|
|
{
|
|
var gbm = new GBM(startPrice: 100.0);
|
|
long startTime = DateTime.UtcNow.Ticks;
|
|
var interval = TimeSpan.FromMinutes(1);
|
|
|
|
var series = gbm.Fetch(5, startTime, interval);
|
|
|
|
// Verify time sequence
|
|
for (int i = 1; i < series.Count; i++)
|
|
{
|
|
Assert.True(series[i].Time > series[i - 1].Time);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Fetch_RespectsInterval()
|
|
{
|
|
var gbm = new GBM(startPrice: 100.0);
|
|
var interval = TimeSpan.FromHours(1);
|
|
long startTime = DateTime.UtcNow.Ticks;
|
|
|
|
var series = gbm.Fetch(5, startTime, interval);
|
|
|
|
// Verify interval spacing
|
|
for (int i = 1; i < series.Count; i++)
|
|
{
|
|
long expectedDiff = interval.Ticks;
|
|
long actualDiff = series[i].Time - series[i - 1].Time;
|
|
Assert.Equal(expectedDiff, actualDiff);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Fetch_StartsAtSpecifiedTime()
|
|
{
|
|
var gbm = new GBM(startPrice: 100.0);
|
|
var startTime = new DateTime(2024, 1, 1, 9, 30, 0, DateTimeKind.Utc).Ticks;
|
|
var interval = TimeSpan.FromMinutes(5);
|
|
|
|
var series = gbm.Fetch(3, startTime, interval);
|
|
|
|
Assert.Equal(startTime, series[0].Time);
|
|
Assert.Equal(startTime + interval.Ticks, series[1].Time);
|
|
Assert.Equal(startTime + 2 * interval.Ticks, series[2].Time);
|
|
}
|
|
|
|
[Fact]
|
|
public void Fetch_WithDifferentIntervals_WorksCorrectly()
|
|
{
|
|
var gbm = new GBM(startPrice: 100.0);
|
|
long startTime = DateTime.UtcNow.Ticks;
|
|
|
|
// Test different intervals
|
|
var intervals = new[] {
|
|
TimeSpan.FromMinutes(1),
|
|
TimeSpan.FromMinutes(5),
|
|
TimeSpan.FromHours(1)
|
|
};
|
|
|
|
foreach (var interval in intervals)
|
|
{
|
|
var series = gbm.Fetch(3, startTime, interval);
|
|
|
|
// Verify spacing
|
|
for (int i = 1; i < series.Count; i++)
|
|
{
|
|
long expectedDiff = interval.Ticks;
|
|
long actualDiff = series[i].Time - series[i - 1].Time;
|
|
Assert.Equal(expectedDiff, actualDiff);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void GeneratesRealisticOHLCV()
|
|
{
|
|
var gbm = new GBM(startPrice: 100.0);
|
|
long startTime = DateTime.UtcNow.Ticks;
|
|
var interval = TimeSpan.FromMinutes(1);
|
|
var series = gbm.Fetch(10, startTime, interval);
|
|
|
|
for (int i = 0; i < series.Count; i++)
|
|
{
|
|
var bar = series[i];
|
|
|
|
// High should be >= max(Open, Close)
|
|
Assert.True(bar.High >= Math.Max(bar.Open, bar.Close));
|
|
|
|
// Low should be <= min(Open, Close)
|
|
Assert.True(bar.Low <= Math.Min(bar.Open, bar.Close));
|
|
|
|
// Volume should be positive
|
|
Assert.True(bar.Volume > 0);
|
|
|
|
// All prices should be positive
|
|
Assert.True(bar.Open > 0);
|
|
Assert.True(bar.High > 0);
|
|
Assert.True(bar.Low > 0);
|
|
Assert.True(bar.Close > 0);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void IntraBarUpdates_ModifyCurrentBar()
|
|
{
|
|
var gbm = new GBM(startPrice: 100.0);
|
|
|
|
var bar1 = gbm.Next(isNew: true);
|
|
long initialTime = bar1.Time;
|
|
double initialClose = bar1.Close;
|
|
|
|
// Loop until price changes (random walk might stay same but unlikely)
|
|
bool changed = false;
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
var bar = gbm.Next(isNew: false);
|
|
Assert.Equal(initialTime, bar.Time);
|
|
if (Math.Abs(bar.Close - initialClose) > double.Epsilon)
|
|
{
|
|
changed = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
Assert.True(changed, "Price should change during intra-bar updates");
|
|
}
|
|
|
|
[Fact]
|
|
public void MixedStreamingAndBatch_WorksCorrectly()
|
|
{
|
|
var gbm = new GBM(startPrice: 100.0);
|
|
|
|
// Start with streaming
|
|
var bar1 = gbm.Next();
|
|
var bar2 = gbm.Next();
|
|
|
|
// Batch generation with explicit time
|
|
long startTime = bar2.Time + TimeSpan.FromMinutes(1).Ticks;
|
|
var interval = TimeSpan.FromMinutes(1);
|
|
var series = gbm.Fetch(3, startTime, interval);
|
|
|
|
Assert.True(series[0].Time > bar2.Time);
|
|
Assert.Equal(3, series.Count);
|
|
|
|
// Continue streaming after batch (uses internal state)
|
|
var bar3 = gbm.Next();
|
|
Assert.True(bar3.Time > series[2].Time);
|
|
}
|
|
|
|
[Fact]
|
|
public void DriftAndVolatility_AffectPriceMovement()
|
|
{
|
|
// High volatility should produce more price variation
|
|
var gbmLowVol = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.01);
|
|
var gbmHighVol = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.5);
|
|
|
|
long startTime = DateTime.UtcNow.Ticks;
|
|
var interval = TimeSpan.FromMinutes(1);
|
|
var seriesLow = gbmLowVol.Fetch(100, startTime, interval);
|
|
var seriesHigh = gbmHighVol.Fetch(100, startTime, interval);
|
|
|
|
// Calculate price ranges
|
|
double rangeLow = seriesLow[99].Close - seriesLow[0].Open;
|
|
double rangeHigh = seriesHigh[99].Close - seriesHigh[0].Open;
|
|
|
|
// High volatility should generally produce larger absolute movements
|
|
Assert.True(Math.Abs(rangeHigh) > Math.Abs(rangeLow) * 0.5);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConsecutiveCalls_MaintainContinuity()
|
|
{
|
|
var gbm = new GBM(startPrice: 100.0);
|
|
|
|
var bar1 = gbm.Next();
|
|
var bar2 = gbm.Next();
|
|
|
|
// bar2.Open should equal bar1.Close (continuity)
|
|
Assert.Equal(bar1.Close, bar2.Open);
|
|
}
|
|
|
|
[Fact]
|
|
public void Stateless_NoHistoryStorage()
|
|
{
|
|
var gbm = new GBM(startPrice: 100.0);
|
|
|
|
// Generate multiple bars
|
|
for (int i = 0; i < 100; i++)
|
|
{
|
|
gbm.Next();
|
|
}
|
|
|
|
// GBM should not expose any history storage
|
|
var type = gbm.GetType();
|
|
var barsProperty = type.GetProperty("Bars");
|
|
|
|
Assert.Null(barsProperty);
|
|
}
|
|
}
|