SIMD Refactor: Merge simd-dev into dev (#55)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4) <aider@aider.chat>
Co-authored-by: Warp <agent@warp.dev>
This commit is contained in:
Miha Kralj
2026-01-18 19:02:03 -08:00
committed by GitHub
co-authored by Claude Opus 4.5 aider Warp
parent 5bcdf8d614
commit 86fe32a682
1750 changed files with 198235 additions and 80539 deletions
+71
View File
@@ -0,0 +1,71 @@
# GBM Class
`GBM` (Geometric Brownian Motion) is a synthetic data generator that simulates realistic financial price movements. It is useful for testing indicators, strategies, and system performance without relying on external data files.
## Key Features
* **Geometric Brownian Motion**: Uses the standard mathematical model for asset price dynamics.
* **Configurable Parameters**: Control drift (trend) and volatility (noise).
* **Stateless Design**: Minimal memory footprint; only maintains state needed for continuity.
* **Dual Modes**: Supports both streaming (bar-by-bar) and batch generation.
* **Intra-bar Updates**: Can simulate real-time price updates within a single bar.
## Mathematical Model
The price evolution follows the stochastic differential equation:
$$ dS_t = \mu S_t dt + \sigma S_t dW_t $$
Where:
* $S_t$: Asset price at time $t$
* $\mu$: Drift (expected return)
* $\sigma$: Volatility (standard deviation of returns)
* $W_t$: Wiener process (Brownian motion)
## Class Definition
```csharp
public class GBM : IFeed
{
public GBM(double startPrice = 100.0, double mu = 0.05, double sigma = 0.2, TimeSpan? defaultTimeframe = null);
public TBar Next(bool isNew = true);
public TBarSeries Fetch(int count, long startTime, TimeSpan interval);
}
```
## Usage
### 1. Initialization
```csharp
// Default: Start at 100, 5% drift, 20% volatility
var gbm = new GBM();
// Custom: Start at 50, 10% drift, 50% volatility
var volatileGbm = new GBM(startPrice: 50.0, mu: 0.10, sigma: 0.50);
```
### 2. Streaming Generation
```csharp
// Generate a new bar
var bar = gbm.Next(isNew: true);
// Simulate intra-bar updates (e.g., real-time ticks)
for (int i = 0; i < 5; i++)
{
var updatedBar = gbm.Next(isNew: false);
Console.WriteLine($"Update: {updatedBar.Close}");
}
```
### 3. Batch Generation
```csharp
long startTime = DateTime.UtcNow.Ticks;
var interval = TimeSpan.FromMinutes(1);
// Generate 1000 bars
var history = gbm.Fetch(1000, startTime, interval);
+703
View File
@@ -0,0 +1,703 @@
namespace QuanTAlib.Tests;
public class GBMTests
{
#region Constructor Tests
[Fact]
public void Constructor_DefaultParameters_CreatesValidInstance()
{
var gbm = new GBM();
Assert.Equal(100.0, gbm.StartPrice);
Assert.Equal(0.05, gbm.Mu);
Assert.Equal(0.2, gbm.Sigma);
Assert.Equal(100.0, gbm.CurrentPrice);
Assert.False(gbm.HasCurrentBar);
}
[Fact]
public void Constructor_CustomParameters_SetsCorrectly()
{
var gbm = new GBM(startPrice: 50.0, mu: 0.1, sigma: 0.3, seed: 42);
Assert.Equal(50.0, gbm.StartPrice);
Assert.Equal(0.1, gbm.Mu);
Assert.Equal(0.3, gbm.Sigma);
Assert.Equal(50.0, gbm.CurrentPrice);
}
[Theory]
[InlineData(0)]
[InlineData(-1)]
[InlineData(-100)]
public void Constructor_InvalidStartPrice_ThrowsArgumentOutOfRangeException(double startPrice)
{
Assert.Throws<ArgumentOutOfRangeException>(() => new GBM(startPrice: startPrice));
}
[Fact]
public void Constructor_NaNStartPrice_ThrowsArgumentOutOfRangeException()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new GBM(startPrice: double.NaN));
}
[Fact]
public void Constructor_InfinityStartPrice_ThrowsArgumentOutOfRangeException()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new GBM(startPrice: double.PositiveInfinity));
Assert.Throws<ArgumentOutOfRangeException>(() => new GBM(startPrice: double.NegativeInfinity));
}
[Theory]
[InlineData(-0.01)]
[InlineData(-1)]
public void Constructor_NegativeSigma_ThrowsArgumentOutOfRangeException(double sigma)
{
Assert.Throws<ArgumentOutOfRangeException>(() => new GBM(sigma: sigma));
}
[Fact]
public void Constructor_NaNSigma_ThrowsArgumentOutOfRangeException()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new GBM(sigma: double.NaN));
}
[Fact]
public void Constructor_InfinitySigma_ThrowsArgumentOutOfRangeException()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new GBM(sigma: double.PositiveInfinity));
}
[Fact]
public void Constructor_NaNMu_ThrowsArgumentOutOfRangeException()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new GBM(mu: double.NaN));
}
[Fact]
public void Constructor_InfinityMu_ThrowsArgumentOutOfRangeException()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new GBM(mu: double.PositiveInfinity));
Assert.Throws<ArgumentOutOfRangeException>(() => new GBM(mu: double.NegativeInfinity));
}
[Fact]
public void Constructor_ZeroTimeframe_ThrowsArgumentOutOfRangeException()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new GBM(defaultTimeframe: TimeSpan.Zero));
}
[Fact]
public void Constructor_NegativeTimeframe_ThrowsArgumentOutOfRangeException()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new GBM(defaultTimeframe: TimeSpan.FromMinutes(-1)));
}
[Fact]
public void Constructor_ZeroSigma_IsValid()
{
var gbm = new GBM(sigma: 0);
Assert.Equal(0, gbm.Sigma);
}
[Fact]
public void Constructor_NegativeMu_IsValid()
{
var gbm = new GBM(mu: -0.1);
Assert.Equal(-0.1, gbm.Mu);
}
#endregion
#region Next Method Tests
[Fact]
public void Next_DefaultParameter_GeneratesNewBar()
{
var gbm = new GBM(startPrice: 100.0, seed: 42);
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, seed: 42);
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, seed: 42);
var bar1 = gbm.Next(isNew: true);
long initialTime = bar1.Time;
var bar2 = gbm.Next(isNew: false);
Assert.Equal(initialTime, bar2.Time);
Assert.Equal(bar1.Open, bar2.Open);
// High/Low/Close/Volume may change
}
[Fact]
public void Next_RefBool_HonorsRequest()
{
var gbm = new GBM(startPrice: 100.0, seed: 42);
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 Next_FirstCallWithIsNewFalse_GeneratesBar()
{
var gbm = new GBM(startPrice: 100.0, seed: 42);
// First call with isNew=false should still generate a bar
var bar = gbm.Next(isNew: false);
Assert.True(bar.Time > 0);
Assert.True(bar.Open > 0);
Assert.True(gbm.HasCurrentBar);
}
[Fact]
public void Next_MultipleUpdates_AccumulatesVolume()
{
var gbm = new GBM(startPrice: 100.0, seed: 42);
var bar1 = gbm.Next(isNew: true);
double initialVolume = bar1.Volume;
var bar2 = gbm.Next(isNew: false);
Assert.True(bar2.Volume > initialVolume, "Volume should accumulate on intra-bar updates");
}
[Fact]
public void Next_IntraBarUpdates_ExpandsHighLow()
{
var gbm = new GBM(startPrice: 100.0, sigma: 0.5, seed: 42);
var bar1 = gbm.Next(isNew: true);
double initialHigh = bar1.High;
double initialLow = bar1.Low;
// Multiple updates should potentially expand the range
for (int i = 0; i < 100; i++)
{
var bar = gbm.Next(isNew: false);
Assert.True(bar.High >= initialHigh || bar.Low <= initialLow || i > 50,
"High-Low range should expand or stay same with updates");
}
}
#endregion
#region Fetch Method Tests
[Fact]
public void Fetch_GeneratesCorrectCount()
{
var gbm = new GBM(startPrice: 100.0, seed: 42);
const 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, seed: 42);
long startTime = DateTime.UtcNow.Ticks;
var interval = TimeSpan.FromMinutes(1);
var series = gbm.Fetch(5, startTime, interval);
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, seed: 42);
var interval = TimeSpan.FromHours(1);
long startTime = DateTime.UtcNow.Ticks;
var series = gbm.Fetch(5, startTime, interval);
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, seed: 42);
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);
}
[Theory]
[InlineData(0)]
[InlineData(-1)]
[InlineData(-100)]
public void Fetch_InvalidCount_ThrowsArgumentException(int count)
{
var gbm = new GBM(startPrice: 100.0);
long startTime = DateTime.UtcNow.Ticks;
var interval = TimeSpan.FromMinutes(1);
Assert.Throws<ArgumentException>(() => gbm.Fetch(count, startTime, interval));
}
[Fact]
public void Fetch_ZeroInterval_ThrowsArgumentOutOfRangeException()
{
var gbm = new GBM(startPrice: 100.0);
long startTime = DateTime.UtcNow.Ticks;
Assert.Throws<ArgumentOutOfRangeException>(() => gbm.Fetch(10, startTime, TimeSpan.Zero));
}
[Fact]
public void Fetch_NegativeInterval_ThrowsArgumentOutOfRangeException()
{
var gbm = new GBM(startPrice: 100.0);
long startTime = DateTime.UtcNow.Ticks;
Assert.Throws<ArgumentOutOfRangeException>(() => gbm.Fetch(10, startTime, TimeSpan.FromMinutes(-1)));
}
[Fact]
public void Fetch_WithDifferentIntervals_WorksCorrectly()
{
var gbm = new GBM(startPrice: 100.0, seed: 42);
long startTime = DateTime.UtcNow.Ticks;
var intervals = new[] {
TimeSpan.FromMinutes(1),
TimeSpan.FromMinutes(5),
TimeSpan.FromHours(1)
};
foreach (var interval in intervals)
{
var series = gbm.Fetch(3, startTime, interval);
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_LargeCount_WorksCorrectly()
{
var gbm = new GBM(startPrice: 100.0, seed: 42);
long startTime = DateTime.UtcNow.Ticks;
var interval = TimeSpan.FromMinutes(1);
var series = gbm.Fetch(10000, startTime, interval);
Assert.Equal(10000, series.Count);
Assert.All(Enumerable.Range(0, series.Count), i =>
{
Assert.True(series[i].Open > 0);
Assert.True(series[i].High > 0);
Assert.True(series[i].Low > 0);
Assert.True(series[i].Close > 0);
Assert.True(series[i].Volume > 0);
});
}
#endregion
#region OHLCV Validity Tests
[Fact]
public void GeneratesRealisticOHLCV()
{
var gbm = new GBM(startPrice: 100.0, seed: 42);
long startTime = DateTime.UtcNow.Ticks;
var interval = TimeSpan.FromMinutes(1);
var series = gbm.Fetch(100, 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),
$"Bar {i}: High ({bar.High}) should be >= max(Open, Close) ({Math.Max(bar.Open, bar.Close)})");
// Low should be <= min(Open, Close)
Assert.True(bar.Low <= Math.Min(bar.Open, bar.Close),
$"Bar {i}: Low ({bar.Low}) should be <= min(Open, Close) ({Math.Min(bar.Open, bar.Close)})");
// High should be >= Low
Assert.True(bar.High >= bar.Low,
$"Bar {i}: High ({bar.High}) should be >= Low ({bar.Low})");
// Volume should be positive
Assert.True(bar.Volume > 0, $"Bar {i}: Volume should be positive");
// All prices should be positive and finite
Assert.True(double.IsFinite(bar.Open) && bar.Open > 0, $"Bar {i}: Open should be positive and finite");
Assert.True(double.IsFinite(bar.High) && bar.High > 0, $"Bar {i}: High should be positive and finite");
Assert.True(double.IsFinite(bar.Low) && bar.Low > 0, $"Bar {i}: Low should be positive and finite");
Assert.True(double.IsFinite(bar.Close) && bar.Close > 0, $"Bar {i}: Close should be positive and finite");
}
}
[Fact]
public void ConsecutiveCalls_MaintainContinuity()
{
var gbm = new GBM(startPrice: 100.0, seed: 42);
var previousBar = gbm.Next();
var currentBar = gbm.Next();
// currentBar.Open should equal previousBar.Close (continuity)
Assert.Equal(previousBar.Close, currentBar.Open);
}
[Fact]
public void Fetch_MaintainsContinuity()
{
var gbm = new GBM(startPrice: 100.0, seed: 42);
long startTime = DateTime.UtcNow.Ticks;
var interval = TimeSpan.FromMinutes(1);
var series = gbm.Fetch(10, startTime, interval);
for (int i = 1; i < series.Count; i++)
{
Assert.True(Math.Abs(series[i - 1].Close - series[i].Open) < 1e-10,
$"Bar {i}: Open should equal previous bar's Close for continuity");
}
}
#endregion
#region Reset Tests
[Fact]
public void Reset_RestoresInitialState()
{
var gbm = new GBM(startPrice: 100.0, seed: 42);
// Generate some bars
gbm.Next();
gbm.Next();
gbm.Next();
Assert.NotEqual(100.0, gbm.CurrentPrice);
Assert.True(gbm.HasCurrentBar);
// Reset
gbm.Reset();
Assert.Equal(100.0, gbm.CurrentPrice);
Assert.False(gbm.HasCurrentBar);
}
[Fact]
public void Reset_WithStartTime_SetsSpecificTime()
{
var gbm = new GBM(startPrice: 100.0, seed: 42);
long specificTime = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks;
gbm.Next();
gbm.Reset(specificTime);
var bar = gbm.Next();
// The bar time should be based on the reset time
Assert.True(bar.Time > specificTime);
Assert.Equal(100.0, bar.Open); // Should start from initial price
}
#endregion
#region Seeded Reproducibility Tests
[Fact]
public void SeededGenerator_ProducesReproducibleResults()
{
var gbm1 = new GBM(startPrice: 100.0, seed: 42);
var gbm2 = new GBM(startPrice: 100.0, seed: 42);
long startTime = DateTime.UtcNow.Ticks;
var interval = TimeSpan.FromMinutes(1);
var series1 = gbm1.Fetch(10, startTime, interval);
var series2 = gbm2.Fetch(10, startTime, interval);
for (int i = 0; i < series1.Count; i++)
{
Assert.Equal(series1[i].Open, series2[i].Open);
Assert.Equal(series1[i].High, series2[i].High);
Assert.Equal(series1[i].Low, series2[i].Low);
Assert.Equal(series1[i].Close, series2[i].Close);
Assert.Equal(series1[i].Volume, series2[i].Volume);
}
}
[Fact]
public void DifferentSeeds_ProduceDifferentResults()
{
var gbm1 = new GBM(startPrice: 100.0, seed: 42);
var gbm2 = new GBM(startPrice: 100.0, seed: 123);
long startTime = DateTime.UtcNow.Ticks;
var interval = TimeSpan.FromMinutes(1);
var series1 = gbm1.Fetch(10, startTime, interval);
var series2 = gbm2.Fetch(10, startTime, interval);
bool anyDifferent = false;
for (int i = 0; i < series1.Count; i++)
{
if (Math.Abs(series1[i].Close - series2[i].Close) > 1e-14)
{
anyDifferent = true;
break;
}
}
Assert.True(anyDifferent, "Different seeds should produce different results");
}
[Fact]
public void UnseededGenerator_ProducesVariableResults()
{
var gbm1 = new GBM(startPrice: 100.0);
var gbm2 = new GBM(startPrice: 100.0);
// Note: This test may occasionally fail due to randomness, but is extremely unlikely
var bar1 = gbm1.Next();
var bar2 = gbm2.Next();
// At least one value should be different (use tolerance for floating-point comparison)
const double tolerance = 1e-14;
bool anyDifferent = Math.Abs(bar1.Close - bar2.Close) > tolerance ||
Math.Abs(bar1.High - bar2.High) > tolerance ||
Math.Abs(bar1.Low - bar2.Low) > tolerance ||
Math.Abs(bar1.Volume - bar2.Volume) > tolerance;
Assert.True(anyDifferent, "Unseeded generators should produce different results");
}
#endregion
#region Drift and Volatility Tests
[Fact]
public void DriftAndVolatility_AffectPriceMovement()
{
var gbmLowVol = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.01, seed: 42);
var gbmHighVol = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.5, seed: 42);
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 standard deviation of returns
double[] returnsLow = new double[99];
double[] returnsHigh = new double[99];
for (int i = 1; i < 100; i++)
{
returnsLow[i - 1] = Math.Log(seriesLow[i].Close / seriesLow[i - 1].Close);
returnsHigh[i - 1] = Math.Log(seriesHigh[i].Close / seriesHigh[i - 1].Close);
}
double stdLow = CalculateStdDev(returnsLow);
double stdHigh = CalculateStdDev(returnsHigh);
Assert.True(stdHigh > stdLow, "High volatility should produce larger return dispersion");
}
[Fact]
public void ZeroVolatility_ProducesConstantPrices()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.0, seed: 42);
long startTime = DateTime.UtcNow.Ticks;
var interval = TimeSpan.FromMinutes(1);
var series = gbm.Fetch(10, startTime, interval);
// With zero volatility and zero drift, price should stay constant
for (int i = 0; i < series.Count; i++)
{
Assert.Equal(100.0, series[i].Close, 10);
}
}
private static double CalculateStdDev(double[] values)
{
double mean = 0;
for (int i = 0; i < values.Length; i++)
mean += values[i];
mean /= values.Length;
double sumSquares = 0;
for (int i = 0; i < values.Length; i++)
sumSquares += (values[i] - mean) * (values[i] - mean);
return Math.Sqrt(sumSquares / values.Length);
}
#endregion
#region State Management Tests
[Fact]
public void IntraBarUpdates_ModifyCurrentBar()
{
var gbm = new GBM(startPrice: 100.0, seed: 42);
var bar1 = gbm.Next(isNew: true);
long initialTime = bar1.Time;
double initialClose = bar1.Close;
bool changed = false;
const double tolerance = 1e-14;
for (int i = 0; i < 10; i++)
{
var bar = gbm.Next(isNew: false);
Assert.Equal(initialTime, bar.Time);
if (Math.Abs(bar.Close - initialClose) > tolerance)
{
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, seed: 42);
_ = gbm.Next();
var bar2 = gbm.Next();
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);
var bar3 = gbm.Next();
Assert.True(bar3.Time > series[2].Time);
}
[Fact]
public void Fetch_ResetsStreamingState()
{
var gbm = new GBM(startPrice: 100.0, seed: 42);
// Create a bar with intra-bar updates
gbm.Next(isNew: true);
gbm.Next(isNew: false);
Assert.True(gbm.HasCurrentBar);
// Fetch should reset streaming state
long startTime = DateTime.UtcNow.Ticks;
gbm.Fetch(5, startTime, TimeSpan.FromMinutes(1));
Assert.False(gbm.HasCurrentBar);
}
#endregion
#region IFeed Interface Tests
[Fact]
public void ImplementsIFeed()
{
GBM feed = new GBM(startPrice: 100.0, seed: 42);
var bar1 = feed.Next(isNew: true);
Assert.True(bar1.Time > 0);
var bar2 = feed.Next(isNew: true);
Assert.True(bar2.Time > bar1.Time);
long startTime = DateTime.UtcNow.Ticks;
var series = feed.Fetch(5, startTime, TimeSpan.FromMinutes(1));
Assert.Equal(5, series.Count);
}
#endregion
#region Statelessness Tests
[Fact]
public void Stateless_NoHistoryStorage()
{
var gbm = new GBM(startPrice: 100.0);
for (int i = 0; i < 100; i++)
{
_ = gbm.Next();
}
var type = typeof(GBM);
var barsProperty = type.GetProperty("Bars");
Assert.Null(barsProperty);
}
#endregion
}
+436
View File
@@ -0,0 +1,436 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib.Tests;
/// <summary>
/// Provides validation utilities for comparing indicator results against external libraries.
/// Contains tolerance constants and verification methods for cross-library validation.
/// </summary>
public static class ValidationHelper
{
/// <summary>
/// Default tolerance for floating-point comparisons (1e-7).
/// Suitable for most indicator comparisons.
/// </summary>
public const double DefaultTolerance = 1e-7;
/// <summary>
/// Tolerance for Ooples Finance library comparisons (1e-7).
/// May need adjustment for specific indicators with different internal precision.
/// </summary>
public const double OoplesTolerance = 1e-7;
/// <summary>
/// Tolerance for Skender.Stock.Indicators library comparisons (1e-7).
/// Skender uses decimal internally, so some precision loss is expected.
/// </summary>
public const double SkenderTolerance = 1e-7;
/// <summary>
/// Tolerance for TA-Lib (TALib.NETCore) library comparisons (1e-7).
/// TA-Lib uses double precision throughout.
/// </summary>
public const double TalibTolerance = 1e-7;
/// <summary>
/// Tolerance for Tulip library comparisons (1e-7).
/// Note: Tulip may have 1-bar shifts due to different initialization strategies.
/// </summary>
public const double TulipTolerance = 1e-7;
/// <summary>
/// Relative tolerance for percentage-based comparisons (0.5%).
/// Use when absolute tolerance is not appropriate.
/// </summary>
public const double RelativeTolerance = 0.005;
/// <summary>
/// Default number of bars to verify from the end of the series.
/// Using 100 bars ensures we're comparing converged values.
/// </summary>
public const int DefaultVerificationCount = 100;
/// <summary>
/// Verifies TSeries results against an external library's results.
/// Compares the last 'skip' values by default.
/// </summary>
/// <typeparam name="TResult">The type of results from the external library</typeparam>
/// <param name="qSeries">QuanTAlib TSeries results</param>
/// <param name="sSeries">External library results</param>
/// <param name="selector">Function to extract the comparable value from external results</param>
/// <param name="skip">Number of values to verify from the end (default: 100)</param>
/// <param name="tolerance">Tolerance for floating-point comparison</param>
public static void VerifyData<TResult>(
TSeries qSeries,
IReadOnlyList<TResult> sSeries,
Func<TResult, double?> selector,
int skip = DefaultVerificationCount,
double tolerance = DefaultTolerance)
{
Assert.Equal(qSeries.Count, sSeries.Count);
int count = qSeries.Count;
int start = Math.Max(0, count - skip);
for (int i = start; i < count; i++)
{
double qValue = qSeries[i].Value;
double? sValue = selector(sSeries[i]);
if (!sValue.HasValue) continue;
Assert.True(
Math.Abs(qValue - sValue.Value) <= tolerance,
$"Mismatch at index {i}: QuanTAlib={qValue:G17}, External={sValue.Value:G17}, Diff={Math.Abs(qValue - sValue.Value):G17}");
}
}
/// <summary>
/// Verifies IReadOnlyList results against an external library's results.
/// </summary>
public static void VerifyData<TResult>(
IReadOnlyList<double> qResults,
IReadOnlyList<TResult> sSeries,
Func<TResult, double?> selector,
int skip = DefaultVerificationCount,
double tolerance = DefaultTolerance)
{
Assert.Equal(qResults.Count, sSeries.Count);
int count = qResults.Count;
int start = Math.Max(0, count - skip);
for (int i = start; i < count; i++)
{
double qValue = qResults[i];
double? sValue = selector(sSeries[i]);
if (!sValue.HasValue) continue;
Assert.True(
Math.Abs(qValue - sValue.Value) <= tolerance,
$"Mismatch at index {i}: QuanTAlib={qValue:G17}, External={sValue.Value:G17}, Diff={Math.Abs(qValue - sValue.Value):G17}");
}
}
/// <summary>
/// Verifies double array results against an external library's results.
/// </summary>
public static void VerifyData<TResult>(
double[] qOutput,
IReadOnlyList<TResult> sSeries,
Func<TResult, double?> selector,
int skip = DefaultVerificationCount,
double tolerance = DefaultTolerance)
{
Assert.Equal(qOutput.Length, sSeries.Count);
int count = qOutput.Length;
int start = Math.Max(0, count - skip);
for (int i = start; i < count; i++)
{
double qValue = qOutput[i];
double? sValue = selector(sSeries[i]);
if (!sValue.HasValue) continue;
Assert.True(
Math.Abs(qValue - sValue.Value) <= tolerance,
$"Mismatch at index {i}: QuanTAlib={qValue:G17}, External={sValue.Value:G17}, Diff={Math.Abs(qValue - sValue.Value):G17}");
}
}
/// <summary>
/// Verifies TSeries results against TA-Lib style output with lookback offset.
/// </summary>
/// <param name="qSeries">QuanTAlib TSeries results</param>
/// <param name="tOutput">TA-Lib output array</param>
/// <param name="lookback">TA-Lib lookback period (output is shifted by this amount)</param>
/// <param name="skip">Number of values to verify from the end</param>
/// <param name="tolerance">Tolerance for floating-point comparison</param>
public static void VerifyData(
TSeries qSeries,
double[] tOutput,
int lookback,
int skip = DefaultVerificationCount,
double tolerance = DefaultTolerance)
{
int count = qSeries.Count;
int start = Math.Max(0, count - skip);
for (int i = start; i < count; i++)
{
double qValue = qSeries[i].Value;
if (i < lookback) continue;
int tIndex = i - lookback;
if (tIndex >= tOutput.Length) continue;
double tValue = tOutput[tIndex];
Assert.True(
Math.Abs(qValue - tValue) <= tolerance,
$"Mismatch at index {i} (TA-Lib index {tIndex}): QuanTAlib={qValue:G17}, TA-Lib={tValue:G17}, Diff={Math.Abs(qValue - tValue):G17}");
}
}
/// <summary>
/// Verifies IReadOnlyList results against TA-Lib style output with lookback offset.
/// </summary>
public static void VerifyData(
IReadOnlyList<double> qResults,
double[] tOutput,
int lookback,
int skip = DefaultVerificationCount,
double tolerance = DefaultTolerance)
{
int count = qResults.Count;
int start = Math.Max(0, count - skip);
for (int i = start; i < count; i++)
{
double qValue = qResults[i];
if (i < lookback) continue;
int tIndex = i - lookback;
if (tIndex >= tOutput.Length) continue;
double tValue = tOutput[tIndex];
Assert.True(
Math.Abs(qValue - tValue) <= tolerance,
$"Mismatch at index {i} (TA-Lib index {tIndex}): QuanTAlib={qValue:G17}, TA-Lib={tValue:G17}, Diff={Math.Abs(qValue - tValue):G17}");
}
}
/// <summary>
/// Verifies double array results against TA-Lib style output with lookback offset.
/// </summary>
public static void VerifyData(
double[] qOutput,
double[] tOutput,
int lookback,
int skip = DefaultVerificationCount,
double tolerance = DefaultTolerance)
{
int count = qOutput.Length;
int start = Math.Max(0, count - skip);
for (int i = start; i < count; i++)
{
double qValue = qOutput[i];
if (i < lookback) continue;
int tIndex = i - lookback;
if (tIndex >= tOutput.Length) continue;
double tValue = tOutput[tIndex];
Assert.True(
Math.Abs(qValue - tValue) <= tolerance,
$"Mismatch at index {i} (TA-Lib index {tIndex}): QuanTAlib={qValue:G17}, TA-Lib={tValue:G17}, Diff={Math.Abs(qValue - tValue):G17}");
}
}
/// <summary>
/// Verifies TSeries results against TA-Lib style output with range and lookback.
/// </summary>
public static void VerifyData(
TSeries qSeries,
double[] tOutput,
Range outRange,
int lookback,
int skip = DefaultVerificationCount,
double tolerance = DefaultTolerance)
{
int count = qSeries.Count;
int start = Math.Max(0, count - skip);
var (offset, length) = outRange.GetOffsetAndLength(tOutput.Length);
for (int i = start; i < count; i++)
{
double qValue = qSeries[i].Value;
if (i < lookback) continue;
int tIndex = i - offset;
if (tIndex < 0 || tIndex >= length) continue;
double tValue = tOutput[tIndex];
Assert.True(
Math.Abs(qValue - tValue) <= tolerance,
$"Mismatch at index {i} (TA-Lib index {tIndex}): QuanTAlib={qValue:G17}, TA-Lib={tValue:G17}, Diff={Math.Abs(qValue - tValue):G17}");
}
}
/// <summary>
/// Verifies IReadOnlyList results against TA-Lib style output with range and lookback.
/// </summary>
public static void VerifyData(
IReadOnlyList<double> qResults,
double[] tOutput,
Range outRange,
int lookback,
int skip = DefaultVerificationCount,
double tolerance = DefaultTolerance)
{
int count = qResults.Count;
int start = Math.Max(0, count - skip);
var (offset, length) = outRange.GetOffsetAndLength(tOutput.Length);
for (int i = start; i < count; i++)
{
double qValue = qResults[i];
if (i < lookback) continue;
int tIndex = i - offset;
if (tIndex < 0 || tIndex >= length) continue;
double tValue = tOutput[tIndex];
Assert.True(
Math.Abs(qValue - tValue) <= tolerance,
$"Mismatch at index {i} (TA-Lib index {tIndex}): QuanTAlib={qValue:G17}, TA-Lib={tValue:G17}, Diff={Math.Abs(qValue - tValue):G17}");
}
}
/// <summary>
/// Verifies double array results against TA-Lib style output with range and lookback.
/// </summary>
public static void VerifyData(
double[] qOutput,
double[] tOutput,
Range outRange,
int lookback,
int skip = DefaultVerificationCount,
double tolerance = DefaultTolerance)
{
int count = qOutput.Length;
int start = Math.Max(0, count - skip);
var (offset, length) = outRange.GetOffsetAndLength(tOutput.Length);
for (int i = start; i < count; i++)
{
double qValue = qOutput[i];
if (i < lookback) continue;
int tIndex = i - offset;
if (tIndex < 0 || tIndex >= length) continue;
double tValue = tOutput[tIndex];
Assert.True(
Math.Abs(qValue - tValue) <= tolerance,
$"Mismatch at index {i} (TA-Lib index {tIndex}): QuanTAlib={qValue:G17}, TA-Lib={tValue:G17}, Diff={Math.Abs(qValue - tValue):G17}");
}
}
/// <summary>
/// Verifies that all values in the series are finite (not NaN or Infinity).
/// </summary>
/// <param name="series">The series to verify</param>
/// <param name="startIndex">Starting index for verification (default: 0)</param>
public static void VerifyAllFinite(TSeries series, int startIndex = 0)
{
for (int i = startIndex; i < series.Count; i++)
{
Assert.True(
double.IsFinite(series[i].Value),
$"Non-finite value at index {i}: {series[i].Value}");
}
}
/// <summary>
/// Verifies that all values in the array are finite (not NaN or Infinity).
/// </summary>
/// <param name="values">The array to verify</param>
/// <param name="startIndex">Starting index for verification (default: 0)</param>
public static void VerifyAllFinite(double[] values, int startIndex = 0)
{
for (int i = startIndex; i < values.Length; i++)
{
Assert.True(
double.IsFinite(values[i]),
$"Non-finite value at index {i}: {values[i]}");
}
}
/// <summary>
/// Verifies that two series produce the same results (for consistency testing).
/// </summary>
/// <param name="series1">First series</param>
/// <param name="series2">Second series</param>
/// <param name="tolerance">Tolerance for floating-point comparison</param>
public static void VerifySeriesEqual(TSeries series1, TSeries series2, double tolerance = DefaultTolerance)
{
Assert.Equal(series1.Count, series2.Count);
for (int i = 0; i < series1.Count; i++)
{
Assert.True(
Math.Abs(series1[i].Value - series2[i].Value) <= tolerance,
$"Mismatch at index {i}: Series1={series1[i].Value:G17}, Series2={series2[i].Value:G17}");
}
}
/// <summary>
/// Calculates the maximum absolute difference between two series.
/// Useful for debugging tolerance issues.
/// </summary>
public static double MaxAbsoluteDifference<TResult>(
TSeries qSeries,
IReadOnlyList<TResult> sSeries,
Func<TResult, double?> selector)
{
if (qSeries.Count != sSeries.Count)
throw new ArgumentException("Series must have the same count", nameof(sSeries));
double maxDiff = 0;
for (int i = 0; i < qSeries.Count; i++)
{
double? sValue = selector(sSeries[i]);
if (!sValue.HasValue) continue;
double diff = Math.Abs(qSeries[i].Value - sValue.Value);
if (diff > maxDiff)
maxDiff = diff;
}
return maxDiff;
}
/// <summary>
/// Calculates the maximum relative difference between two series.
/// Useful for percentage-based tolerance testing.
/// </summary>
public static double MaxRelativeDifference<TResult>(
TSeries qSeries,
IReadOnlyList<TResult> sSeries,
Func<TResult, double?> selector)
{
if (qSeries.Count != sSeries.Count)
throw new ArgumentException("Series must have the same count", nameof(sSeries));
double maxDiff = 0;
for (int i = 0; i < qSeries.Count; i++)
{
double? sValue = selector(sSeries[i]);
if (!sValue.HasValue || Math.Abs(sValue.Value) < double.Epsilon) continue;
double relDiff = Math.Abs((qSeries[i].Value - sValue.Value) / sValue.Value);
if (relDiff > maxDiff)
maxDiff = relDiff;
}
return maxDiff;
}
}
+219
View File
@@ -0,0 +1,219 @@
using Skender.Stock.Indicators;
namespace QuanTAlib.Tests;
/// <summary>
/// Provides standardized test data for validation tests.
/// Uses GBM (Geometric Brownian Motion) to generate realistic price data
/// and converts it to formats required by external validation libraries.
/// </summary>
public sealed class ValidationTestData : IDisposable
{
/// <summary>
/// Default number of bars for validation tests.
/// 5000 bars ensures sufficient convergence for most indicators.
/// </summary>
public const int DefaultCount = 5000;
/// <summary>
/// Default starting price for generated data.
/// </summary>
public const double DefaultStartPrice = 1000.0;
/// <summary>
/// Default annual drift for GBM (5%).
/// </summary>
public const double DefaultMu = 0.05;
/// <summary>
/// Default annual volatility for GBM (200%).
/// High volatility ensures diverse price scenarios.
/// </summary>
public const double DefaultSigma = 2.0;
/// <summary>
/// Default random seed for reproducibility.
/// </summary>
public const int DefaultSeed = 123;
/// <summary>
/// Gets the generated bar series.
/// </summary>
public TBarSeries Bars { get; }
/// <summary>
/// Gets the close price series.
/// </summary>
public TSeries Data { get; }
/// <summary>
/// Gets the quotes in Skender.Stock.Indicators format.
/// </summary>
public IReadOnlyList<Quote> SkenderQuotes { get; }
/// <summary>
/// Gets the raw close price data as a ReadOnlyMemory for span-based APIs.
/// </summary>
public ReadOnlyMemory<double> RawData { get; }
/// <summary>
/// Gets the raw open prices as read-only memory.
/// </summary>
public ReadOnlyMemory<double> OpenPrices { get; }
/// <summary>
/// Gets the raw high prices as read-only memory.
/// </summary>
public ReadOnlyMemory<double> HighPrices { get; }
/// <summary>
/// Gets the raw low prices as read-only memory.
/// </summary>
public ReadOnlyMemory<double> LowPrices { get; }
/// <summary>
/// Gets the raw close prices as read-only memory.
/// </summary>
public ReadOnlyMemory<double> ClosePrices { get; }
/// <summary>
/// Gets the raw volume data as read-only memory.
/// </summary>
public ReadOnlyMemory<double> VolumeData { get; }
/// <summary>
/// Gets the timestamps as read-only memory.
/// </summary>
public ReadOnlyMemory<long> Timestamps { get; }
/// <summary>
/// Gets the number of bars in the dataset.
/// </summary>
public int Count => Bars.Count;
/// <summary>
/// Creates validation test data with default parameters.
/// </summary>
public ValidationTestData()
: this(DefaultCount, DefaultStartPrice, DefaultMu, DefaultSigma, DefaultSeed)
{
}
/// <summary>
/// Creates validation test data with specified parameters.
/// </summary>
/// <param name="count">Number of bars to generate</param>
/// <param name="startPrice">Starting price</param>
/// <param name="mu">Annual drift rate</param>
/// <param name="sigma">Annual volatility</param>
/// <param name="seed">Random seed for reproducibility</param>
public ValidationTestData(
int count,
double startPrice = DefaultStartPrice,
double mu = DefaultMu,
double sigma = DefaultSigma,
int seed = DefaultSeed)
{
var gbm = new GBM(startPrice, mu, sigma, seed: seed);
Bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
Data = Bars.Close;
// Extract raw arrays efficiently (avoid LINQ in hot path)
int barCount = Bars.Count;
var openPrices = new double[barCount];
var highPrices = new double[barCount];
var lowPrices = new double[barCount];
var closePrices = new double[barCount];
var volumeData = new double[barCount];
var timestamps = new long[barCount];
// Use span-based access for efficiency
var openSpan = Bars.OpenValues;
var highSpan = Bars.HighValues;
var lowSpan = Bars.LowValues;
var closeSpan = Bars.CloseValues;
var volumeSpan = Bars.VolumeValues;
var timeSpan = Bars.Times;
openSpan.CopyTo(openPrices);
highSpan.CopyTo(highPrices);
lowSpan.CopyTo(lowPrices);
closeSpan.CopyTo(closePrices);
volumeSpan.CopyTo(volumeData);
timeSpan.CopyTo(timestamps);
// Expose as ReadOnlyMemory to prevent external modification
OpenPrices = openPrices;
HighPrices = highPrices;
LowPrices = lowPrices;
ClosePrices = closePrices;
VolumeData = volumeData;
Timestamps = timestamps;
RawData = closePrices;
// Build Skender quotes without LINQ
var quotes = new Quote[barCount];
for (int i = 0; i < barCount; i++)
{
quotes[i] = new Quote
{
Date = new DateTime(timestamps[i], DateTimeKind.Utc),
Open = (decimal)openPrices[i],
High = (decimal)highPrices[i],
Low = (decimal)lowPrices[i],
Close = (decimal)closePrices[i],
Volume = (decimal)volumeData[i],
};
}
SkenderQuotes = quotes;
}
/// <summary>
/// Creates a new ValidationTestData instance with the specified bar count.
/// Note: This regenerates data using the same seed rather than slicing existing data,
/// ensuring deterministic results but not reusing the parent's generated bars.
/// </summary>
/// <param name="count">Number of bars to generate (must be between 1 and current Count)</param>
/// <returns>A new ValidationTestData instance with freshly generated data</returns>
public ValidationTestData CreateSubset(int count)
{
if (count <= 0 || count > Count)
throw new ArgumentOutOfRangeException(nameof(count), count, $"Count must be between 1 and {Count}");
return new ValidationTestData(count, DefaultStartPrice, DefaultMu, DefaultSigma, DefaultSeed);
}
/// <summary>
/// Gets the close price span for SIMD operations.
/// </summary>
public ReadOnlySpan<double> GetCloseSpan() => ClosePrices.Span;
/// <summary>
/// Gets the high price span for SIMD operations.
/// </summary>
public ReadOnlySpan<double> GetHighSpan() => HighPrices.Span;
/// <summary>
/// Gets the low price span for SIMD operations.
/// </summary>
public ReadOnlySpan<double> GetLowSpan() => LowPrices.Span;
/// <summary>
/// Gets the open price span for SIMD operations.
/// </summary>
public ReadOnlySpan<double> GetOpenSpan() => OpenPrices.Span;
/// <summary>
/// Gets the volume span for SIMD operations.
/// </summary>
public ReadOnlySpan<double> GetVolumeSpan() => VolumeData.Span;
/// <summary>
/// Disposes of resources (no-op, but implements pattern for test fixtures).
/// </summary>
public void Dispose()
{
// No unmanaged resources to dispose
// Implemented for IDisposable pattern compatibility with test fixtures
}
}
+348
View File
@@ -0,0 +1,348 @@
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
namespace QuanTAlib;
/// <summary>
/// Geometric Brownian Motion (GBM) generator for simulating OHLCV data.
/// Generates realistic price data for testing indicators and strategies.
/// Stateless design - only maintains minimal state needed for price continuity.
/// </summary>
[SkipLocalsInit]
#pragma warning disable S101 // Rename class 'GBM' to match pascal case naming rules
#pragma warning disable S2245 // Random is acceptable for simulation/testing purposes
public sealed class GBM : IFeed
#pragma warning restore S101
{
private readonly Random? _rnd;
private double _lastPrice;
private long _lastTime;
private readonly double _drift;
private readonly double _vol;
private readonly long _defaultTimeStep;
private TBar _currentBar;
private bool _hasCurrentBar;
private double _cachedZ;
private bool _hasCachedZ;
/// <summary>
/// Gets the annual drift/return rate.
/// </summary>
public double Mu { get; }
/// <summary>
/// Gets the annual volatility.
/// </summary>
public double Sigma { get; }
/// <summary>
/// Gets the starting price.
/// </summary>
public double StartPrice { get; }
/// <summary>
/// Gets the current price state.
/// </summary>
public double CurrentPrice => _lastPrice;
/// <summary>
/// Gets whether the generator has a current bar in progress.
/// </summary>
public bool HasCurrentBar => _hasCurrentBar;
/// <summary>
/// Creates a new GBM generator.
/// </summary>
/// <param name="startPrice">Initial price (default: 100.0, must be positive and finite)</param>
/// <param name="mu">Annual drift/return rate (default: 0.05 = 5%, must be finite)</param>
/// <param name="sigma">Annual volatility (default: 0.2 = 20%, must be non-negative and finite)</param>
/// <param name="defaultTimeframe">Default timeframe for bars (default: 1 minute, must be positive)</param>
/// <param name="seed">Optional random seed for reproducibility (default: null for non-deterministic)</param>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when startPrice is not positive/finite, sigma is negative/non-finite,
/// mu is non-finite, or defaultTimeframe is non-positive.
/// </exception>
public GBM(
double startPrice = 100.0,
double mu = 0.05,
double sigma = 0.2,
TimeSpan? defaultTimeframe = null,
int? seed = null)
{
// Validate startPrice
if (startPrice <= 0 || !double.IsFinite(startPrice))
throw new ArgumentOutOfRangeException(nameof(startPrice), startPrice, "Start price must be positive and finite");
// Validate mu
if (!double.IsFinite(mu))
throw new ArgumentOutOfRangeException(nameof(mu), mu, "Drift (mu) must be finite");
// Validate sigma
if (sigma < 0 || !double.IsFinite(sigma))
throw new ArgumentOutOfRangeException(nameof(sigma), sigma, "Volatility (sigma) must be non-negative and finite");
// Use provided timeframe or default to 1 minute
var timeframe = defaultTimeframe ?? TimeSpan.FromMinutes(1);
// Validate timeframe
if (timeframe <= TimeSpan.Zero)
throw new ArgumentOutOfRangeException(nameof(defaultTimeframe), defaultTimeframe, "Timeframe must be positive");
_rnd = seed.HasValue ? new Random(seed.Value) : null;
StartPrice = startPrice;
_lastPrice = startPrice;
_lastTime = DateTime.UtcNow.Ticks;
Mu = mu;
Sigma = sigma;
_defaultTimeStep = timeframe.Ticks;
const double minutesPerYear = 252.0 * 6.5 * 60.0;
double dt = timeframe.TotalMinutes / minutesPerYear;
_drift = (mu - 0.5 * sigma * sigma) * dt;
_vol = sigma * Math.Sqrt(dt);
}
/// <summary>
/// Resets the generator to its initial state.
/// </summary>
public void Reset()
{
_lastPrice = StartPrice;
_lastTime = DateTime.UtcNow.Ticks;
_currentBar = default;
_hasCurrentBar = false;
_cachedZ = 0;
_hasCachedZ = false;
}
/// <summary>
/// Resets the generator to its initial state with a specific start time.
/// </summary>
/// <param name="startTime">The start time in ticks.</param>
public void Reset(long startTime)
{
_lastPrice = StartPrice;
_lastTime = startTime;
_currentBar = default;
_hasCurrentBar = false;
_cachedZ = 0;
_hasCachedZ = false;
}
/// <summary>
/// Generates a random double in [0, 1) using either the seeded Random or RandomNumberGenerator.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double NextDouble()
{
if (_rnd != null)
{
return _rnd.NextDouble();
}
Span<byte> buffer = stackalloc byte[8];
RandomNumberGenerator.Fill(buffer);
ulong ul = BitConverter.ToUInt64(buffer);
return (ul >> 11) * (1.0 / (1ul << 53));
}
/// <summary>
/// Generates next standard normal using Box-Muller transform with caching.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double NextNormal()
{
if (_hasCachedZ)
{
_hasCachedZ = false;
return _cachedZ;
}
double u1 = 1.0 - NextDouble();
double u2 = 1.0 - NextDouble();
// Guard against log(0) which produces -Infinity
if (u1 <= double.Epsilon)
u1 = double.Epsilon;
double mag = Math.Sqrt(-2.0 * Math.Log(u1));
double angle = 2.0 * Math.PI * u2;
_cachedZ = mag * Math.Sin(angle);
_hasCachedZ = true;
return mag * Math.Cos(angle);
}
/// <summary>
/// Gets the next bar with full bidirectional control.
/// GBM always honors the request - isNew parameter unchanged on return.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TBar Next(ref bool isNew)
{
// GBM always honors request - parameter unchanged
if (isNew || !_hasCurrentBar)
{
// Generate new bar
long currentTime = _lastTime + _defaultTimeStep;
double z = NextNormal();
double price = _lastPrice * Math.Exp(Math.FusedMultiplyAdd(_vol, z, _drift));
// Ensure price stays positive and finite
if (!double.IsFinite(price) || price <= 0)
price = _lastPrice;
double volume = 1000 + NextDouble() * 1000;
double open = _lastPrice;
double close = price;
double rnd1 = NextDouble();
double rnd2 = NextDouble();
double high = Math.Max(open, close) * (1.0 + rnd1 * 0.01);
double low = Math.Min(open, close) * (1.0 - rnd2 * 0.01);
// Ensure valid OHLC constraints
high = Math.Max(high, Math.Max(open, close));
low = Math.Min(low, Math.Min(open, close));
low = Math.Max(double.Epsilon, low); // Ensure positive
_currentBar = new TBar(currentTime, open, high, low, close, volume);
_hasCurrentBar = true;
_lastPrice = close;
_lastTime = currentTime;
}
else
{
// Update current bar (intra-bar tick)
double z = NextNormal();
double price = _lastPrice * Math.Exp(Math.FusedMultiplyAdd(_vol, z, _drift));
// Ensure price stays positive and finite
if (!double.IsFinite(price) || price <= 0)
price = _lastPrice;
double additionalVolume = 1000 + NextDouble() * 1000;
var bar = _currentBar;
double newClose = price;
double newHigh = Math.Max(bar.High, newClose);
double newLow = Math.Min(bar.Low, newClose);
newLow = Math.Max(double.Epsilon, newLow); // Ensure positive
double newVolume = bar.Volume + additionalVolume;
_currentBar = new TBar(bar.Time, bar.Open, newHigh, newLow, newClose, newVolume);
_lastPrice = newClose;
}
return _currentBar;
}
/// <summary>
/// Gets the next bar with simple control.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TBar Next(bool isNew = true)
{
// Delegate to ref version
return Next(ref isNew);
}
/// <summary>
/// Generates a batch of bars using optimized batch processing with explicit time parameters.
/// </summary>
/// <param name="count">Number of bars to generate (must be positive)</param>
/// <param name="startTime">Starting timestamp in ticks</param>
/// <param name="interval">Time interval between bars (must be positive)</param>
/// <returns>A TBarSeries containing the generated bars</returns>
/// <exception cref="ArgumentException">Thrown when count is not positive</exception>
/// <exception cref="ArgumentOutOfRangeException">Thrown when interval is not positive</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TBarSeries Fetch(int count, long startTime, TimeSpan interval)
{
if (count <= 0)
throw new ArgumentException("Count must be positive", nameof(count));
if (interval <= TimeSpan.Zero)
throw new ArgumentOutOfRangeException(nameof(interval), interval, "Interval must be positive");
var series = new TBarSeries(count);
// Pre-allocate arrays for SoA layout
long[] t = new long[count];
double[] o = new double[count];
double[] h = new double[count];
double[] l = new double[count];
double[] c = new double[count];
double[] v = new double[count];
const double minutesPerYear = 252.0 * 6.5 * 60.0;
double dt = interval.TotalMinutes / minutesPerYear;
double drift = (Mu - 0.5 * Sigma * Sigma) * dt;
double vol = Sigma * Math.Sqrt(dt);
long timeStep = interval.Ticks;
double currentPrice = _lastPrice;
long currentTime = startTime;
for (int i = 0; i < count; i++)
{
double z = NextNormal();
double price = currentPrice * Math.Exp(Math.FusedMultiplyAdd(vol, z, drift));
// Ensure price stays positive and finite
if (!double.IsFinite(price) || price <= 0)
price = currentPrice;
double open = currentPrice;
double close = price;
double rnd1 = NextDouble();
double rnd2 = NextDouble();
double rnd3 = NextDouble();
t[i] = currentTime;
o[i] = open;
c[i] = close;
double high = Math.Max(open, close) * (1.0 + rnd1 * 0.01);
double low = Math.Min(open, close) * (1.0 - rnd2 * 0.01);
// Ensure valid OHLC constraints
high = Math.Max(high, Math.Max(open, close));
low = Math.Min(low, Math.Min(open, close));
low = Math.Max(double.Epsilon, low); // Ensure positive
h[i] = high;
l[i] = low;
v[i] = 1000 + rnd3 * 1000;
currentPrice = price;
currentTime += timeStep;
}
// Update internal state to continue from end of batch
_lastPrice = currentPrice;
_lastTime = currentTime - timeStep; // Last bar time, not next bar time
// Bulk add to series
series.Add(t, o, h, l, c, v);
// Reset streaming state after batch
_hasCurrentBar = false;
return series;
}
}
#pragma warning restore S2245