Files
QuanTAlib/lib/feeds/gbm/ValidationTestData.cs
2026-02-28 14:14:35 -08:00

222 lines
7.1 KiB
C#

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.
/// 10000 bars ensures sufficient convergence for most indicators.
/// </summary>
public const int DefaultCount = 10000;
/// <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
}
}