using System.Buffers; using System.Runtime.CompilerServices; using System.Security.Cryptography; namespace QuanTAlib; /// /// 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. /// [SkipLocalsInit] #pragma warning disable S101 // Rename class 'GBM' to match pascal case naming rules 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; /// /// Gets the annual drift/return rate. /// public double Mu { get; } /// /// Gets the annual volatility. /// public double Sigma { get; } /// /// Gets the starting price. /// public double StartPrice { get; } /// /// Gets the current price state. /// public double CurrentPrice => _lastPrice; /// /// Gets whether the generator has a current bar in progress. /// public bool HasCurrentBar => _hasCurrentBar; /// /// Creates a new GBM generator. /// /// Initial price (default: 100.0, must be positive and finite) /// Annual drift/return rate (default: 0.05 = 5%, must be finite) /// Annual volatility (default: 0.2 = 20%, must be non-negative and finite) /// Default timeframe for bars (default: 1 minute, must be positive) /// Optional random seed for reproducibility (default: null for non-deterministic) /// /// Thrown when startPrice is not positive/finite, sigma is negative/non-finite, /// mu is non-finite, or defaultTimeframe is non-positive. /// 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); } /// /// Resets the generator to its initial state. /// /// /// Sets the internal time anchor to . For deterministic /// time sequences use with an explicit start time. /// public void Reset() { _lastPrice = StartPrice; _lastTime = DateTime.UtcNow.Ticks; _currentBar = default; _hasCurrentBar = false; _cachedZ = 0; _hasCachedZ = false; } /// /// Resets the generator to its initial state with a specific start time. /// /// The start time in ticks. public void Reset(long startTime) { _lastPrice = StartPrice; _lastTime = startTime; _currentBar = default; _hasCurrentBar = false; _cachedZ = 0; _hasCachedZ = false; } /// /// Generates a random double in [0, 1) using either the seeded Random or RandomNumberGenerator. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private double NextDouble() { if (_rnd != null) { return _rnd.NextDouble(); } Span buffer = stackalloc byte[8]; RandomNumberGenerator.Fill(buffer); ulong ul = BitConverter.ToUInt64(buffer); return (ul >> 11) * (1.0 / (1ul << 53)); } /// /// Generates next standard normal using Box-Muller transform with caching. /// [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); } /// /// Gets the next bar with full bidirectional control. /// GBM always honors the request - isNew parameter unchanged on return. /// [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; } /// /// Gets the next bar with simple control. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public TBar Next(bool isNew = true) { // Delegate to ref version return Next(ref isNew); } /// /// Generates a batch of bars using optimized batch processing with explicit time parameters. /// Uses stackalloc for small batches to avoid heap allocations. /// /// Number of bars to generate (must be positive) /// Starting timestamp in ticks /// Time interval between bars (must be positive) /// A TBarSeries containing the generated bars /// Thrown when count is not positive /// Thrown when interval is not positive /// /// Price continuity: batch[0].Open equals _lastPrice at call time, so the /// batch begins exactly where the previous call left off. /// After the call, _lastPrice and _lastTime are updated to the end of the /// generated batch, enabling seamless continuation via subsequent /// calls. need not follow the previous _lastTime — /// this allows replaying a window or generating a non-contiguous batch while preserving /// price continuity. /// [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); // Threshold for stackalloc: 64 bars * (8 bytes for long + 5*8 bytes for doubles) = 64 * 48 = 3KB // Stay well under typical stack limit; use 64 as safe threshold const int StackAllocThreshold = 64; // Use stackalloc for small batches to avoid heap allocations if (count <= StackAllocThreshold) { Span t = stackalloc long[count]; Span o = stackalloc double[count]; Span h = stackalloc double[count]; Span l = stackalloc double[count]; Span c = stackalloc double[count]; Span v = stackalloc double[count]; FetchCore(count, startTime, interval, t, o, h, l, c, v); // Bulk add to series using ReadOnlySpan overload series.AddRange(t, o, h, l, c, v); } else { // Use ArrayPool for larger batches to avoid heap allocations long[]? rentedT = null; double[]? rentedO = null; double[]? rentedH = null; double[]? rentedL = null; double[]? rentedC = null; double[]? rentedV = null; try { rentedT = ArrayPool.Shared.Rent(count); rentedO = ArrayPool.Shared.Rent(count); rentedH = ArrayPool.Shared.Rent(count); rentedL = ArrayPool.Shared.Rent(count); rentedC = ArrayPool.Shared.Rent(count); rentedV = ArrayPool.Shared.Rent(count); // Use only the first 'count' elements (rented arrays may be larger) var t = rentedT.AsSpan(0, count); var o = rentedO.AsSpan(0, count); var h = rentedH.AsSpan(0, count); var l = rentedL.AsSpan(0, count); var c = rentedC.AsSpan(0, count); var v = rentedV.AsSpan(0, count); FetchCore(count, startTime, interval, t, o, h, l, c, v); // Bulk add to series using ReadOnlySpan overload series.AddRange(t, o, h, l, c, v); } finally { if (rentedT != null) { ArrayPool.Shared.Return(rentedT); } if (rentedO != null) { ArrayPool.Shared.Return(rentedO); } if (rentedH != null) { ArrayPool.Shared.Return(rentedH); } if (rentedL != null) { ArrayPool.Shared.Return(rentedL); } if (rentedC != null) { ArrayPool.Shared.Return(rentedC); } if (rentedV != null) { ArrayPool.Shared.Return(rentedV); } } } // Reset streaming state after batch _hasCurrentBar = false; return series; } /// /// Core generation logic shared between stackalloc and heap-allocated paths. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void FetchCore(int count, long startTime, TimeSpan interval, Span t, Span o, Span h, Span l, Span c, Span v) { 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 } }