From e6033638ad37281e2359eb80882b8ad1968a1e4d Mon Sep 17 00:00:00 2001 From: Miha Kralj Date: Wed, 10 Dec 2025 21:58:45 -0500 Subject: [PATCH] Refactor trend indicators to use record structs for state management --- .clinerules/good-indicator.md | 22 ++++-- AGENTS.md | 7 +- lib/feeds/gbm/gbm.cs | 40 +++++++--- lib/trends/alma/Alma.cs | 23 ++++-- lib/trends/conv/Conv.cs | 29 ++++--- lib/trends/dema/Dema.cs | 21 +---- lib/trends/ema/Ema.cs | 21 +---- lib/trends/kama/Kama.cs | 89 ++++++++++++--------- lib/trends/lsma/Lsma.cs | 72 +++++++---------- lib/trends/mama/Mama.cs | 109 ++++++++++---------------- lib/trends/sma/Sma.cs | 58 ++++++-------- lib/trends/t3/T3.cs | 20 +---- lib/trends/tema/Tema.cs | 21 +---- lib/trends/trima/Trima.cs | 140 ++++++++++++++++++---------------- lib/trends/vidya/Vidya.cs | 110 +++++++++----------------- lib/trends/wma/Wma.cs | 69 ++++++++--------- test_record_struct.cs | 23 ++++++ 17 files changed, 390 insertions(+), 484 deletions(-) create mode 100644 test_record_struct.cs diff --git a/.clinerules/good-indicator.md b/.clinerules/good-indicator.md index 3bfd4eb3..f90512c1 100644 --- a/.clinerules/good-indicator.md +++ b/.clinerules/good-indicator.md @@ -39,8 +39,9 @@ Each indicator resides in its own directory such as `lib/trends/`, `lib/indicato ### State Management -* Use `RingBuffer` for sliding window data. -* Maintain separate state variables for the *current* calculation (`_sum`, `_lastVal`) and the *previous* valid state (`_p_sum`, `_p_lastVal`) to support `isNew=false` updates. +* **Scalar State:** Use a `private record struct State` to group all scalar state variables. This ensures value semantics, automatic `IEquatable` implementation, and cleaner rollback logic. +* **State Variables:** Maintain `private State _state;` (current) and `private State _p_state;` (previous valid state). +* **Buffers:** Use `RingBuffer` for sliding window data. * **Resync:** Implement a periodic full recalculation (e.g., every 1000 ticks) to prevent floating-point drift in running sums. ### Constructor @@ -54,10 +55,17 @@ Each indicator resides in its own directory such as `lib/trends/`, `lib/indicato * **Signature:** `public TValue Update(TValue input, bool isNew = true)` * **Attribute:** `[MethodImpl(MethodImplOptions.AggressiveInlining)]` * **Logic:** - 1. **Input Validation:** Check `double.IsFinite`. If not, use `_lastValidValue`. - 2. **State Management:** - * If `isNew=true`: Save current state to `_p_*` variables, then update. - * If `isNew=false`: Restore state from `_p_*` variables, then update. + 1. **State Rollback:** + ```csharp + if (isNew) { + _p_state = _state; + // ... update state (e.g. counters) ... + } else { + _state = _p_state; + // ... update state ... + } + ``` + 2. **Input Validation:** Check `double.IsFinite`. If not, use `_lastValidValue` (stored in `State`). 3. **Calculation:** Perform the math. 4. **Publish:** Update `Last` property, invoke `Pub` event, return `Last`. @@ -68,7 +76,7 @@ Each indicator resides in its own directory such as `lib/trends/`, `lib/indicato * **Logic:** 1. Create output series. 2. Call static `Calculate(Span)` for performance. - 3. Restore internal state by replaying the last `Period` bars. + 3. Restore internal state by replaying the last `Period` bars (or full series if recursive). ### Static Calculate (TSeries) diff --git a/AGENTS.md b/AGENTS.md index d2bce9a3..b6838c48 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -65,9 +65,10 @@ public TValue Update(TValue input, bool isNew = true) ### State Management -* Use `RingBuffer` for sliding windows. -* Maintain `_state` and `_p_state` (previous state) variables to support `isNew=false` rollbacks. -* **Resync**: Periodically recalculate running sums to prevent floating-point drift. +* **Scalar State:** Use a `private record struct State` to group all scalar state variables. This ensures value semantics, automatic `IEquatable` implementation, and cleaner rollback logic. +* **State Variables:** Maintain `private State _state;` (current) and `private State _p_state;` (previous valid state). +* **Buffers:** Use `RingBuffer` for sliding windows. +* **Resync:** Periodically recalculate running sums to prevent floating-point drift. ### Dual API Requirement diff --git a/lib/feeds/gbm/gbm.cs b/lib/feeds/gbm/gbm.cs index bae2b5c4..755e1ad4 100644 --- a/lib/feeds/gbm/gbm.cs +++ b/lib/feeds/gbm/gbm.cs @@ -1,4 +1,5 @@ using System.Runtime.CompilerServices; +using System.Security.Cryptography; namespace QuanTAlib; @@ -13,7 +14,7 @@ namespace QuanTAlib; public class GBM : IFeed #pragma warning restore S101 { - private readonly Random _rnd; + private readonly Random? _rnd; private double _lastPrice; private long _lastTime; @@ -52,7 +53,7 @@ public class GBM : IFeed ArgumentOutOfRangeException.ThrowIfNegativeOrZero(startPrice); ArgumentOutOfRangeException.ThrowIfNegative(sigma); - _rnd = seed.HasValue ? new Random(seed.Value) : new Random(); + _rnd = seed.HasValue ? new Random(seed.Value) : null; _lastPrice = startPrice; _lastTime = DateTime.UtcNow.Ticks; @@ -71,6 +72,23 @@ public class GBM : IFeed _vol = sigma * Math.Sqrt(dt); } + /// + /// 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. /// @@ -83,8 +101,8 @@ public class GBM : IFeed return _cachedZ; } - double u1 = 1.0 - _rnd.NextDouble(); // nosemgrep - double u2 = 1.0 - _rnd.NextDouble(); // nosemgrep + double u1 = 1.0 - NextDouble(); + double u2 = 1.0 - NextDouble(); double mag = Math.Sqrt(-2.0 * Math.Log(u1)); double angle = 2.0 * Math.PI * u2; @@ -110,12 +128,12 @@ public class GBM : IFeed double z = NextNormal(); double price = _lastPrice * Math.Exp(_drift + _vol * z); - double volume = 1000 + _rnd.NextDouble() * 1000; + double volume = 1000 + NextDouble() * 1000; double open = _lastPrice; double close = price; - double high = Math.Max(open, close) * (1.0 + _rnd.NextDouble() * 0.01); - double low = Math.Min(open, close) * (1.0 - _rnd.NextDouble() * 0.01); + double high = Math.Max(open, close) * (1.0 + NextDouble() * 0.01); + double low = Math.Min(open, close) * (1.0 - NextDouble() * 0.01); _currentBar = new TBar(currentTime, open, high, low, close, volume); _hasCurrentBar = true; @@ -128,7 +146,7 @@ public class GBM : IFeed // Update current bar (intra-bar tick) double z = NextNormal(); double price = _lastPrice * Math.Exp(_drift + _vol * z); - double additionalVolume = 1000 + _rnd.NextDouble() * 1000; + double additionalVolume = 1000 + NextDouble() * 1000; var bar = _currentBar; double newClose = price; @@ -190,9 +208,9 @@ public class GBM : IFeed double open = currentPrice; double close = price; - double rnd1 = _rnd.NextDouble(); - double rnd2 = _rnd.NextDouble(); - double rnd3 = _rnd.NextDouble(); + double rnd1 = NextDouble(); + double rnd2 = NextDouble(); + double rnd3 = NextDouble(); t[i] = currentTime; o[i] = open; diff --git a/lib/trends/alma/Alma.cs b/lib/trends/alma/Alma.cs index a94eb6d5..fdd7c0af 100644 --- a/lib/trends/alma/Alma.cs +++ b/lib/trends/alma/Alma.cs @@ -31,7 +31,10 @@ public sealed class Alma : ITValuePublisher private readonly double[] _weights; private readonly double _weightSum; private readonly RingBuffer _buffer; - private double _lastValidValue; + + private record struct State(double LastValidValue); + private State _state; + private State _p_state; /// /// Display name for the indicator. @@ -99,15 +102,24 @@ public sealed class Alma : ITValuePublisher { if (double.IsFinite(input)) { - _lastValidValue = input; + _state.LastValidValue = input; return input; } - return _lastValidValue; + return _state.LastValidValue; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TValue input, bool isNew = true) { + if (isNew) + { + _p_state = _state; + } + else + { + _state = _p_state; + } + double val = GetValidValue(input.Value); _buffer.Add(val, isNew); @@ -140,7 +152,7 @@ public sealed class Alma : ITValuePublisher // Restore state _buffer.Clear(); - _lastValidValue = 0; + _state = default; // Replay last part to restore buffer state int startIndex = Math.Max(0, len - _period); @@ -301,7 +313,8 @@ public sealed class Alma : ITValuePublisher public void Reset() { _buffer.Clear(); - _lastValidValue = 0; + _state = default; + _p_state = default; Last = default; } } diff --git a/lib/trends/conv/Conv.cs b/lib/trends/conv/Conv.cs index cf591a59..5ce0a952 100644 --- a/lib/trends/conv/Conv.cs +++ b/lib/trends/conv/Conv.cs @@ -25,10 +25,9 @@ public sealed class Conv : ITValuePublisher private readonly double[] _kernel; private readonly RingBuffer _buffer; - private double _lastValidValue; - - // State for bar correction - private double _p_lastValidValue; + private record struct State(double LastValidValue); + private State _state; + private State _p_state; public string Name { get; } public TValue Last { get; private set; } @@ -45,8 +44,8 @@ public sealed class Conv : ITValuePublisher Array.Copy(kernel, _kernel, _period); _buffer = new RingBuffer(_period); Name = $"Conv({_period})"; - _lastValidValue = double.NaN; - _p_lastValidValue = double.NaN; + _state.LastValidValue = double.NaN; + _p_state.LastValidValue = double.NaN; } public Conv(ITValuePublisher source, double[] kernel) : this(kernel) @@ -59,10 +58,10 @@ public sealed class Conv : ITValuePublisher { if (double.IsFinite(input)) { - _lastValidValue = input; + _state.LastValidValue = input; return input; } - return _lastValidValue; + return _state.LastValidValue; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -70,11 +69,11 @@ public sealed class Conv : ITValuePublisher { if (isNew) { - _p_lastValidValue = _lastValidValue; + _p_state = _state; } else { - _lastValidValue = _p_lastValidValue; + _state = _p_state; } double val = GetValidValue(input.Value); @@ -145,14 +144,14 @@ public sealed class Conv : ITValuePublisher { if (double.IsFinite(sourceValues[i])) { - _lastValidValue = sourceValues[i]; + _state.LastValidValue = sourceValues[i]; break; } } } else { - _lastValidValue = double.NaN; + _state.LastValidValue = double.NaN; } _buffer.Clear(); @@ -168,7 +167,7 @@ public sealed class Conv : ITValuePublisher Last = new TValue(source.Times[len - 1], vSpan[len - 1]); // Save state for isNew=false - _p_lastValidValue = _lastValidValue; + _p_state = _state; return new TSeries(t, v); } @@ -241,8 +240,8 @@ public sealed class Conv : ITValuePublisher public void Reset() { _buffer.Clear(); - _lastValidValue = double.NaN; - _p_lastValidValue = double.NaN; + _state.LastValidValue = double.NaN; + _p_state.LastValidValue = double.NaN; Last = default; } } diff --git a/lib/trends/dema/Dema.cs b/lib/trends/dema/Dema.cs index d5a30f89..62c49c64 100644 --- a/lib/trends/dema/Dema.cs +++ b/lib/trends/dema/Dema.cs @@ -24,28 +24,9 @@ namespace QuanTAlib; [SkipLocalsInit] public sealed class Dema : ITValuePublisher { - private struct EmaState : IEquatable + private record struct EmaState(double Ema, double E, bool IsHot, bool IsCompensated) { - public double Ema; - public double E; - public bool IsHot; - public bool IsCompensated; - public static EmaState New() => new() { Ema = 0, E = 1.0, IsHot = false, IsCompensated = false }; - - public override bool Equals(object? obj) => obj is EmaState other && Equals(other); - - public bool Equals(EmaState other) => - Ema == other.Ema && - E == other.E && - IsHot == other.IsHot && - IsCompensated == other.IsCompensated; - - public override int GetHashCode() => HashCode.Combine(Ema, E, IsHot, IsCompensated); - - public static bool operator ==(EmaState left, EmaState right) => left.Equals(right); - - public static bool operator !=(EmaState left, EmaState right) => !left.Equals(right); } private readonly double _alpha; diff --git a/lib/trends/ema/Ema.cs b/lib/trends/ema/Ema.cs index d4641fc8..a72b88f6 100644 --- a/lib/trends/ema/Ema.cs +++ b/lib/trends/ema/Ema.cs @@ -27,28 +27,9 @@ namespace QuanTAlib; [SkipLocalsInit] public sealed class Ema : ITValuePublisher { - private struct State : IEquatable + private record struct State(double Ema, double E, bool IsHot, bool IsCompensated) { - public double Ema; - public double E; - public bool IsHot; - public bool IsCompensated; - public static State New() => new() { Ema = 0, E = 1.0, IsHot = false, IsCompensated = false }; - - public override bool Equals(object? obj) => obj is State other && Equals(other); - - public bool Equals(State other) => - Ema == other.Ema && - E == other.E && - IsHot == other.IsHot && - IsCompensated == other.IsCompensated; - - public override int GetHashCode() => HashCode.Combine(Ema, E, IsHot, IsCompensated); - - public static bool operator ==(State left, State right) => left.Equals(right); - - public static bool operator !=(State left, State right) => !left.Equals(right); } private readonly double _alpha; diff --git a/lib/trends/kama/Kama.cs b/lib/trends/kama/Kama.cs index a7b4fc49..a6c6f48c 100644 --- a/lib/trends/kama/Kama.cs +++ b/lib/trends/kama/Kama.cs @@ -24,12 +24,10 @@ public sealed class Kama : ITValuePublisher private readonly double _fastAlpha; private readonly double _slowAlpha; private readonly RingBuffer _buffer; - private double _kama; - private double _p_kama; - private double _volatilitySum; - private double _p_volatilitySum; - private double _lastDiffOut; - private double _lastValidValue; + + private record struct State(double Kama, double VolatilitySum, double NextDiffOut, double LastValidValue); + private State _state; + private State _p_state; /// /// Display name for the indicator. @@ -74,8 +72,10 @@ public sealed class Kama : ITValuePublisher _slowAlpha = 2.0 / (slowPeriod + 1); Name = $"Kama({period}, {fastPeriod}, {slowPeriod})"; - _kama = double.NaN; - _lastValidValue = double.NaN; + _state.Kama = double.NaN; + _state.LastValidValue = double.NaN; + _p_state.Kama = double.NaN; + _p_state.LastValidValue = double.NaN; } public Kama(ITValuePublisher source, int period = 10, int fastPeriod = 2, int slowPeriod = 30) @@ -89,15 +89,24 @@ public sealed class Kama : ITValuePublisher { if (double.IsFinite(input)) { - _lastValidValue = input; + _state.LastValidValue = input; return input; } - return _lastValidValue; + return _state.LastValidValue; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TValue input, bool isNew = true) { + if (isNew) + { + _p_state = _state; + } + else + { + _state = _p_state; + } + double val = GetValidValue(input.Value); if (double.IsNaN(val)) { @@ -108,55 +117,58 @@ public sealed class Kama : ITValuePublisher if (isNew) { - _p_kama = _kama; - _p_volatilitySum = _volatilitySum; - bool wasFull = _buffer.IsFull; - double removed = _buffer.Add(val); + _buffer.Add(val); if (wasFull) { - double diff_out = Math.Abs(removed - _buffer[0]); - _lastDiffOut = diff_out; - + double diff_out = _p_state.NextDiffOut; double diff_in = Math.Abs(_buffer[^1] - _buffer[^2]); - _volatilitySum += diff_in - diff_out; + _state.VolatilitySum += diff_in - diff_out; + + // Calculate NextDiffOut for the next step + // NextDiffOut = abs(buffer[0] - buffer[1]) + _state.NextDiffOut = Math.Abs(_buffer[0] - _buffer[1]); } else if (_buffer.Count >= 2) { double diff_in = Math.Abs(_buffer[^1] - _buffer[^2]); - _volatilitySum += diff_in; - _lastDiffOut = 0; + _state.VolatilitySum += diff_in; + + if (_buffer.IsFull) + { + // Buffer just became full. + // NextDiffOut = abs(buffer[0] - buffer[1]) + _state.NextDiffOut = Math.Abs(_buffer[0] - _buffer[1]); + } } } else { - // Restore state - _kama = _p_kama; _buffer.UpdateNewest(val); if (_buffer.IsFull) { double diff_in = Math.Abs(_buffer[^1] - _buffer[^2]); - _volatilitySum = _p_volatilitySum + diff_in - _lastDiffOut; + // Use NextDiffOut from _p_state (which is the correct DiffOut for this transition) + _state.VolatilitySum = _p_state.VolatilitySum + diff_in - _p_state.NextDiffOut; } else if (_buffer.Count >= 2) { double diff_in = Math.Abs(_buffer[^1] - _buffer[^2]); - _volatilitySum = _p_volatilitySum + diff_in; + _state.VolatilitySum = _p_state.VolatilitySum + diff_in; } } // Calculate KAMA - if (double.IsNaN(_kama)) + if (double.IsNaN(_state.Kama)) { - _kama = val; - _p_kama = val; // Ensure p_kama is initialized + _state.Kama = val; } else { double change = Math.Abs(_buffer[^1] - _buffer[0]); - double volatility = _volatilitySum; + double volatility = _state.VolatilitySum; // Avoid division by zero double er = (volatility > double.Epsilon) ? change / volatility : 0.0; @@ -166,10 +178,16 @@ public sealed class Kama : ITValuePublisher double sc = er * (_fastAlpha - _slowAlpha) + _slowAlpha; sc *= sc; - _kama = _p_kama + sc * (val - _p_kama); + double prevKama = _p_state.Kama; + if (double.IsNaN(prevKama)) + { + prevKama = _state.Kama; + } + + _state.Kama = prevKama + sc * (val - prevKama); } - Last = new TValue(input.Time, _kama); + Last = new TValue(input.Time, _state.Kama); Pub?.Invoke(Last); return Last; } @@ -290,7 +308,6 @@ public sealed class Kama : ITValuePublisher double change = 0; change = (count == bufSize) ? Math.Abs(val - buffer[bufferIdx]) : Math.Abs(val - buffer[0]); - double er = (volatilitySum > double.Epsilon) ? change / volatilitySum : 0.0; if (er > 1.0) er = 1.0; @@ -306,12 +323,10 @@ public sealed class Kama : ITValuePublisher public void Reset() { _buffer.Clear(); - _kama = double.NaN; - _p_kama = double.NaN; - _volatilitySum = 0; - _p_volatilitySum = 0; - _lastDiffOut = 0; - _lastValidValue = double.NaN; + _state = default; + _state.Kama = double.NaN; + _state.LastValidValue = double.NaN; + _p_state = _state; Last = default; } } diff --git a/lib/trends/lsma/Lsma.cs b/lib/trends/lsma/Lsma.cs index 0470888e..efb891d4 100644 --- a/lib/trends/lsma/Lsma.cs +++ b/lib/trends/lsma/Lsma.cs @@ -34,15 +34,10 @@ public sealed class Lsma : ITValuePublisher private readonly double _sum_x; private readonly double _denominator; - private double _sum_y; - private double _sum_xy; + private record struct State(double SumY, double SumXY, double LastVal, double LastValidValue); + private State _state; + private State _p_state; - private double _p_sum_y; - private double _p_sum_xy; - private double _p_last_val; - - private double _lastValidValue; - private double _p_lastValidValue; private int _tickCount; private const int ResyncInterval = 1000; @@ -100,10 +95,10 @@ public sealed class Lsma : ITValuePublisher { if (double.IsFinite(input)) { - _lastValidValue = input; + _state.LastValidValue = input; return input; } - return _lastValidValue; + return _state.LastValidValue; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -112,24 +107,24 @@ public sealed class Lsma : ITValuePublisher if (_buffer.IsFull) { double oldest = _buffer.Oldest; - double prev_sum_y = _sum_y; + double prev_sum_y = _state.SumY; // O(1) update for sum_xy // sum_xy_new = sum_xy_old + sum_y_prev - n * oldest - _sum_xy = _sum_xy + prev_sum_y - _period * oldest; + _state.SumXY = _state.SumXY + prev_sum_y - _period * oldest; // O(1) update for sum_y - _sum_y = _sum_y - oldest + val; + _state.SumY = _state.SumY - oldest + val; _buffer.Add(val); } else { _buffer.Add(val); - _sum_y += val; + _state.SumY += val; // Recalculate sum_xy from scratch during warmup - _sum_xy = 0; + _state.SumXY = 0; var span = _buffer.GetSpan(); for (int i = 0; i < span.Length; i++) { @@ -138,7 +133,7 @@ public sealed class Lsma : ITValuePublisher // index j in buffer corresponds to x = count - 1 - j // sum_xy = sum(x * y) int x = span.Length - 1 - i; - _sum_xy += x * span[i]; + _state.SumXY += x * span[i]; } } @@ -152,13 +147,13 @@ public sealed class Lsma : ITValuePublisher private void Resync() { - _sum_y = _buffer.Sum; - _sum_xy = 0; + _state.SumY = _buffer.Sum; + _state.SumXY = 0; var span = _buffer.GetSpan(); for (int i = 0; i < span.Length; i++) { int x = span.Length - 1 - i; - _sum_xy += x * span[i]; + _state.SumXY += x * span[i]; } } @@ -170,25 +165,23 @@ public sealed class Lsma : ITValuePublisher double val = GetValidValue(input.Value); UpdateState(val); - _p_sum_y = _sum_y; - _p_sum_xy = _sum_xy; - _p_last_val = val; - _p_lastValidValue = _lastValidValue; + _p_state = _state; + _state.LastVal = val; } else { - _lastValidValue = _p_lastValidValue; + _state.LastValidValue = _p_state.LastValidValue; double val = GetValidValue(input.Value); // For isNew=false, we update the current bar. // sum_xy remains constant because it depends on the previous window state which hasn't changed. // sum_y updates to reflect the change in the newest value. - _sum_y = _p_sum_y - _p_last_val + val; - _sum_xy = _p_sum_xy; // Restore sum_xy to the state after the shift + _state.SumY = _p_state.SumY - _p_state.LastVal + val; + _state.SumXY = _p_state.SumXY; // Restore sum_xy to the state after the shift _buffer.UpdateNewest(val); - _p_last_val = val; + _state.LastVal = val; } double result; @@ -218,8 +211,8 @@ public sealed class Lsma : ITValuePublisher } else { - double m = (n * _sum_xy - sx * _sum_y) / denom; - double b = (_sum_y - m * sx) / n; + double m = (n * _state.SumXY - sx * _state.SumY) / denom; + double b = (_state.SumY - m * sx) / n; // LSMA = b - m * offset result = b - m * _offset; @@ -261,17 +254,17 @@ public sealed class Lsma : ITValuePublisher { if (double.IsFinite(source.Values[i])) { - _lastValidValue = source.Values[i]; + _state.LastValidValue = source.Values[i]; break; } } } else { - _lastValidValue = 0; + _state.LastValidValue = 0; } - double lastProcessedValue = _lastValidValue; + double lastProcessedValue = _state.LastValidValue; for (int i = startIndex; i < len; i++) { double val = GetValidValue(source.Values[i]); @@ -279,10 +272,8 @@ public sealed class Lsma : ITValuePublisher lastProcessedValue = val; } - _p_sum_y = _sum_y; - _p_sum_xy = _sum_xy; - _p_last_val = lastProcessedValue; - _p_lastValidValue = _lastValidValue; + _state.LastVal = lastProcessedValue; + _p_state = _state; Last = new TValue(tSpan[len - 1], vSpan[len - 1]); return new TSeries(t, v); @@ -409,14 +400,9 @@ public sealed class Lsma : ITValuePublisher public void Reset() { _buffer.Clear(); - _sum_y = 0; - _sum_xy = 0; - _p_sum_y = 0; - _p_sum_xy = 0; - _p_last_val = 0; + _state = default; + _p_state = default; Last = default; _tickCount = 0; - _lastValidValue = 0; - _p_lastValidValue = 0; } } diff --git a/lib/trends/mama/Mama.cs b/lib/trends/mama/Mama.cs index 78f388d7..10dcc4c9 100644 --- a/lib/trends/mama/Mama.cs +++ b/lib/trends/mama/Mama.cs @@ -12,25 +12,18 @@ public sealed class Mama : ITValuePublisher { public TValue Last { get; private set; } public TValue Fama { get; private set; } - public bool IsHot => _index > 6; + public bool IsHot => _state.Index > 6; public event Action? Pub; private readonly double _fastLimit; private readonly double _slowLimit; - private double _period, _p_period; - private double _phase, _p_phase; - private double _mama, _p_mama; - private double _fama, _p_fama; - private double _sumPr, _p_sumPr; - private int _index; - - // State variables for IIR filters need to be preserved - private double _i2, _p_i2; - private double _q2, _p_q2; - private double _re, _p_re; - private double _im, _p_im; - private double _lastValidPrice; + private record struct State( + double Period, double Phase, double Mama, double Fama, double SumPr, + double I2, double Q2, double Re, double Im, double LastValidPrice, int Index + ); + private State _state; + private State _p_state; private readonly RingBuffer _priceBuffer; private readonly RingBuffer _smoothBuffer; @@ -69,18 +62,10 @@ public sealed class Mama : ITValuePublisher public void Init() { - _period = _p_period = 0.0; - _phase = _p_phase = 0.0; - _mama = _p_mama = double.NaN; - _fama = _p_fama = double.NaN; - _sumPr = _p_sumPr = 0.0; - _index = 0; - - _i2 = _p_i2 = 0.0; - _q2 = _p_q2 = 0.0; - _re = _p_re = 0.0; - _im = _p_im = 0.0; - _lastValidPrice = 0.0; + _state = default; + _state.Mama = double.NaN; + _state.Fama = double.NaN; + _p_state = _state; _priceBuffer.Clear(); _smoothBuffer.Clear(); @@ -97,45 +82,29 @@ public sealed class Mama : ITValuePublisher { if (isNew) { - _p_period = _period; - _p_phase = _phase; - _p_mama = _mama; - _p_fama = _fama; - _p_sumPr = _sumPr; - _p_i2 = _i2; - _p_q2 = _q2; - _p_re = _re; - _p_im = _im; - _index++; + _p_state = _state; + _state.Index++; } else { - _period = _p_period; - _phase = _p_phase; - _mama = _p_mama; - _fama = _p_fama; - _sumPr = _p_sumPr; - _i2 = _p_i2; - _q2 = _p_q2; - _re = _p_re; - _im = _p_im; + _state = _p_state; } double price = input.Value; if (!double.IsFinite(price)) { - price = _lastValidPrice; + price = _state.LastValidPrice; } else { - _lastValidPrice = price; + _state.LastValidPrice = price; } _priceBuffer.Add(price, isNew); - if (_index > 6) + if (_state.Index > 6) { - double adj = (0.075 * _period) + 0.54; + double adj = (0.075 * _state.Period) + 0.54; // Smooth double smooth = (4.0 * _priceBuffer[0] + 3.0 * _priceBuffer[1] + 2.0 * _priceBuffer[2] + _priceBuffer[3]) * 0.1; @@ -164,50 +133,50 @@ public sealed class Mama : ITValuePublisher double q2_val = q1 + jI; // Smooth i2, q2 - _i2 = 0.2 * i2_val + 0.8 * _p_i2; - _q2 = 0.2 * q2_val + 0.8 * _p_q2; + _state.I2 = 0.2 * i2_val + 0.8 * _p_state.I2; + _state.Q2 = 0.2 * q2_val + 0.8 * _p_state.Q2; // Homodyne discriminator - double re_val = (_i2 * _p_i2) + (_q2 * _p_q2); - double im_val = (_i2 * _p_q2) - (_q2 * _p_i2); + double re_val = (_state.I2 * _p_state.I2) + (_state.Q2 * _p_state.Q2); + double im_val = (_state.I2 * _p_state.Q2) - (_state.Q2 * _p_state.I2); // Smooth re, im - _re = 0.2 * re_val + 0.8 * _p_re; - _im = 0.2 * im_val + 0.8 * _p_im; + _state.Re = 0.2 * re_val + 0.8 * _p_state.Re; + _state.Im = 0.2 * im_val + 0.8 * _p_state.Im; // Calculate Period - double period = (Math.Abs(_im) > double.Epsilon && Math.Abs(_re) > double.Epsilon) - ? TWOPI / Math.Atan(_im / _re) + double period = (Math.Abs(_state.Im) > double.Epsilon && Math.Abs(_state.Re) > double.Epsilon) + ? TWOPI / Math.Atan(_state.Im / _state.Re) : 0.0; // Adjust Period - period = period > 1.5 * _p_period ? 1.5 * _p_period : period; - period = period < 0.67 * _p_period ? 0.67 * _p_period : period; + period = period > 1.5 * _p_state.Period ? 1.5 * _p_state.Period : period; + period = period < 0.67 * _p_state.Period ? 0.67 * _p_state.Period : period; period = period < 6.0 ? 6.0 : period; period = period > 50.0 ? 50.0 : period; // Smooth Period - _period = 0.2 * period + 0.8 * _p_period; + _state.Period = 0.2 * period + 0.8 * _p_state.Period; // Phase calculation - _phase = Math.Abs(i1) >= double.Epsilon ? Math.Atan(q1 / i1) * RadToDeg : 0.0; + _state.Phase = Math.Abs(i1) >= double.Epsilon ? Math.Atan(q1 / i1) * RadToDeg : 0.0; // Adaptive alpha - double delta = Math.Max(_p_phase - _phase, 1.0); + double delta = Math.Max(_p_state.Phase - _state.Phase, 1.0); double alpha = _fastLimit / delta; alpha = Math.Clamp(alpha, _slowLimit, _fastLimit); // Final indicators - _mama = alpha * _priceBuffer[0] + (1.0 - alpha) * _p_mama; - _fama = 0.5 * alpha * _mama + (1.0 - 0.5 * alpha) * _p_fama; + _state.Mama = alpha * _priceBuffer[0] + (1.0 - alpha) * _p_state.Mama; + _state.Fama = 0.5 * alpha * _state.Mama + (1.0 - 0.5 * alpha) * _p_state.Fama; } else { // Initialization phase - _sumPr += price; - double avg = _index > 0 ? _sumPr / _index : price; - _mama = avg; - _fama = avg; + _state.SumPr += price; + double avg = _state.Index > 0 ? _state.SumPr / _state.Index : price; + _state.Mama = avg; + _state.Fama = avg; // Initialize buffers with 0 _smoothBuffer.Add(0, isNew); @@ -216,8 +185,8 @@ public sealed class Mama : ITValuePublisher _Q1_buffer.Add(0, isNew); } - Last = new TValue(input.Time, _mama); - Fama = new TValue(input.Time, _fama); + Last = new TValue(input.Time, _state.Mama); + Fama = new TValue(input.Time, _state.Fama); Pub?.Invoke(Last); return Last; } diff --git a/lib/trends/sma/Sma.cs b/lib/trends/sma/Sma.cs index 86113934..4d6a0e67 100644 --- a/lib/trends/sma/Sma.cs +++ b/lib/trends/sma/Sma.cs @@ -31,12 +31,9 @@ public sealed class Sma : ITValuePublisher private readonly int _period; private readonly RingBuffer _buffer; - private double _sum; - private double _p_sum; - private double _p_lastInput; - private double _lastValidValue; - private double _p_lastValidValue; - private int _tickCount; + private record struct State(double Sum, double LastInput, double LastValidValue, int TickCount); + private State _state; + private State _p_state; private const int ResyncInterval = 1000; @@ -85,10 +82,10 @@ public sealed class Sma : ITValuePublisher { if (double.IsFinite(input)) { - _lastValidValue = input; + _state.LastValidValue = input; return input; } - return _lastValidValue; + return _state.LastValidValue; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -96,19 +93,18 @@ public sealed class Sma : ITValuePublisher { double removedValue = _buffer.Count == _buffer.Capacity ? _buffer.Oldest : 0.0; - _sum = _sum - removedValue + val; + _state.Sum = _state.Sum - removedValue + val; _buffer.Add(val); - _tickCount++; - if (_buffer.IsFull && _tickCount >= ResyncInterval) + _state.TickCount++; + if (_buffer.IsFull && _state.TickCount >= ResyncInterval) { - _tickCount = 0; - _sum = _buffer.RecalculateSum(); + _state.TickCount = 0; + _state.Sum = _buffer.RecalculateSum(); } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TValue input, bool isNew = true) { @@ -116,21 +112,20 @@ public sealed class Sma : ITValuePublisher { double val = GetValidValue(input.Value); UpdateState(val); + _state.LastInput = val; - _p_sum = _sum; - _p_lastInput = val; - _p_lastValidValue = _lastValidValue; + _p_state = _state; } else { - _lastValidValue = _p_lastValidValue; + _state = _p_state; double val = GetValidValue(input.Value); - _sum = _p_sum - _p_lastInput + val; + _state.Sum = _state.Sum - _state.LastInput + val; _buffer.UpdateNewest(val); } - double result = _sum / _buffer.Count; + double result = _state.Sum / _buffer.Count; Last = new TValue(input.Time, result); Pub?.Invoke(Last); return Last; @@ -158,34 +153,33 @@ public sealed class Sma : ITValuePublisher if (startIndex > 0) { - _lastValidValue = 0; + _state.LastValidValue = 0; for (int i = startIndex - 1; i >= 0; i--) { if (double.IsFinite(source.Values[i])) { - _lastValidValue = source.Values[i]; + _state.LastValidValue = source.Values[i]; break; } } } else { - _lastValidValue = 0; + _state.LastValidValue = 0; } _buffer.Clear(); - _sum = 0; - _tickCount = 0; + _state.Sum = 0; + _state.TickCount = 0; for (int i = startIndex; i < len; i++) { double val = GetValidValue(source.Values[i]); UpdateState(val); + _state.LastInput = val; } - _p_sum = _sum; - _p_lastInput = GetValidValue(source.Values[len - 1]); - _p_lastValidValue = _lastValidValue; + _p_state = _state; Last = new TValue(tSpan[len - 1], vSpan[len - 1]); return new TSeries(t, v); @@ -528,12 +522,8 @@ public sealed class Sma : ITValuePublisher public void Reset() { _buffer.Clear(); - _sum = 0; - _p_sum = 0; - _p_lastInput = 0; - _lastValidValue = 0; - _p_lastValidValue = 0; - _tickCount = 0; + _state = default; + _p_state = default; Last = default; } } diff --git a/lib/trends/t3/T3.cs b/lib/trends/t3/T3.cs index 8b6f4510..0bda877d 100644 --- a/lib/trends/t3/T3.cs +++ b/lib/trends/t3/T3.cs @@ -26,27 +26,9 @@ namespace QuanTAlib; [SkipLocalsInit] public sealed class T3 : ITValuePublisher { - private struct State : IEquatable + private record struct State(double E1, double E2, double E3, double E4, double E5, double E6, bool IsInitialized) { - public double E1, E2, E3, E4, E5, E6; - public bool IsInitialized; - public static State New() => new() { IsInitialized = false }; - - public override bool Equals(object? obj) => obj is State other && Equals(other); - -#pragma warning disable S1244 // Do not check floating point equality with exact values - public bool Equals(State other) => - E1 == other.E1 && E2 == other.E2 && E3 == other.E3 && - E4 == other.E4 && E5 == other.E5 && E6 == other.E6 && - IsInitialized == other.IsInitialized; -#pragma warning restore S1244 // Do not check floating point equality with exact values - - public override int GetHashCode() => HashCode.Combine(E1, E2, E3, E4, E5, E6, IsInitialized); - - public static bool operator ==(State left, State right) => left.Equals(right); - - public static bool operator !=(State left, State right) => !left.Equals(right); } private readonly struct Parameters : IEquatable diff --git a/lib/trends/tema/Tema.cs b/lib/trends/tema/Tema.cs index 8620b3f8..f4146162 100644 --- a/lib/trends/tema/Tema.cs +++ b/lib/trends/tema/Tema.cs @@ -26,28 +26,9 @@ namespace QuanTAlib; [SkipLocalsInit] public sealed class Tema : ITValuePublisher { - private struct EmaState : IEquatable + private record struct EmaState(double Ema, double E, bool IsHot, bool IsCompensated) { - public double Ema; - public double E; - public bool IsHot; - public bool IsCompensated; - public static EmaState New() => new() { Ema = 0, E = 1.0, IsHot = false, IsCompensated = false }; - - public override bool Equals(object? obj) => obj is EmaState other && Equals(other); - - public bool Equals(EmaState other) => - Ema == other.Ema && - E == other.E && - IsHot == other.IsHot && - IsCompensated == other.IsCompensated; - - public override int GetHashCode() => HashCode.Combine(Ema, E, IsHot, IsCompensated); - - public static bool operator ==(EmaState left, EmaState right) => left.Equals(right); - - public static bool operator !=(EmaState left, EmaState right) => !left.Equals(right); } private readonly double _alpha; diff --git a/lib/trends/trima/Trima.cs b/lib/trends/trima/Trima.cs index c1098daf..35887e85 100644 --- a/lib/trends/trima/Trima.cs +++ b/lib/trends/trima/Trima.cs @@ -31,18 +31,19 @@ public sealed class Trima : ITValuePublisher private readonly RingBuffer _buffer1; private readonly RingBuffer _buffer2; - private double _sum1, _p_sum1, _p_lastInput1, _lastValidValue1, _p_lastValidValue1; - private int _tickCount1; + private record struct State( + double Sum1, double LastInput1, double LastValidValue1, int TickCount1, double NextRemoved1, + double Sum2, double LastInput2, int TickCount2, double NextRemoved2, + int SampleCount + ); + private State _state; + private State _p_state; - private double _sum2, _p_sum2, _p_lastInput2; - private int _tickCount2; - - private int _sampleCount; private const int ResyncInterval = 1000; public string Name { get; } public TValue Last { get; private set; } - public bool IsHot => _sampleCount >= _period; + public bool IsHot => _state.SampleCount >= _period; public event Action? Pub; public Trima(int period) @@ -69,10 +70,10 @@ public sealed class Trima : ITValuePublisher { if (double.IsFinite(input)) { - _lastValidValue1 = input; + _state.LastValidValue1 = input; return input; } - return _lastValidValue1; + return _state.LastValidValue1; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -80,61 +81,72 @@ public sealed class Trima : ITValuePublisher { if (isNew) { - _sampleCount++; - - // SMA 1 - double val1 = GetValidValue(input.Value); - double removed1 = _buffer1.Count == _buffer1.Capacity ? _buffer1.Oldest : 0.0; - _sum1 = _sum1 - removed1 + val1; - _buffer1.Add(val1); - - _tickCount1++; - if (_buffer1.IsFull && _tickCount1 >= ResyncInterval) - { - _tickCount1 = 0; - _sum1 = _buffer1.Sum(); - } - - _p_sum1 = _sum1; - _p_lastInput1 = val1; - _p_lastValidValue1 = _lastValidValue1; - - double sma1Result = _sum1 / _buffer1.Count; - - // SMA 2 - double removed2 = _buffer2.Count == _buffer2.Capacity ? _buffer2.Oldest : 0.0; - _sum2 = _sum2 - removed2 + sma1Result; - _buffer2.Add(sma1Result); - - _tickCount2++; - if (_buffer2.IsFull && _tickCount2 >= ResyncInterval) - { - _tickCount2 = 0; - _sum2 = _buffer2.Sum(); - } - - _p_sum2 = _sum2; - _p_lastInput2 = sma1Result; - - Last = new TValue(input.Time, _sum2 / _buffer2.Count); + _p_state = _state; + _state.SampleCount++; } else { - // SMA 1 Correction - _lastValidValue1 = _p_lastValidValue1; - double val1 = GetValidValue(input.Value); - _sum1 = _p_sum1 - _p_lastInput1 + val1; - _buffer1.UpdateNewest(val1); - - double sma1Result = _sum1 / _buffer1.Count; - - // SMA 2 Correction - _sum2 = _p_sum2 - _p_lastInput2 + sma1Result; - _buffer2.UpdateNewest(sma1Result); - - Last = new TValue(input.Time, _sum2 / _buffer2.Count); + _state = _p_state; } + // SMA 1 + double val1 = GetValidValue(input.Value); + + if (isNew) + { + double removed1 = _buffer1.Count == _buffer1.Capacity ? _buffer1.Oldest : 0.0; + _state.Sum1 = _state.Sum1 - removed1 + val1; + _buffer1.Add(val1); + + // Store NextRemoved1 for next step + _state.NextRemoved1 = _buffer1.Count == _buffer1.Capacity ? _buffer1.Oldest : 0.0; + + _state.TickCount1++; + if (_buffer1.IsFull && _state.TickCount1 >= ResyncInterval) + { + _state.TickCount1 = 0; + _state.Sum1 = _buffer1.Sum(); + } + } + else + { + // Use NextRemoved1 from _p_state + double removed1 = _p_state.NextRemoved1; + _state.Sum1 = _p_state.Sum1 - removed1 + val1; + _buffer1.UpdateNewest(val1); + } + + _state.LastInput1 = val1; + double sma1Result = _state.Sum1 / _buffer1.Count; + + // SMA 2 + if (isNew) + { + double removed2 = _buffer2.Count == _buffer2.Capacity ? _buffer2.Oldest : 0.0; + _state.Sum2 = _state.Sum2 - removed2 + sma1Result; + _buffer2.Add(sma1Result); + + // Store NextRemoved2 for next step + _state.NextRemoved2 = _buffer2.Count == _buffer2.Capacity ? _buffer2.Oldest : 0.0; + + _state.TickCount2++; + if (_buffer2.IsFull && _state.TickCount2 >= ResyncInterval) + { + _state.TickCount2 = 0; + _state.Sum2 = _buffer2.Sum(); + } + } + else + { + // Use NextRemoved2 from _p_state + double removed2 = _p_state.NextRemoved2; + _state.Sum2 = _p_state.Sum2 - removed2 + sma1Result; + _buffer2.UpdateNewest(sma1Result); + } + + _state.LastInput2 = sma1Result; + + Last = new TValue(input.Time, _state.Sum2 / _buffer2.Count); Pub?.Invoke(Last); return Last; } @@ -203,14 +215,8 @@ public sealed class Trima : ITValuePublisher { _buffer1.Clear(); _buffer2.Clear(); - - _sum1 = _p_sum1 = _p_lastInput1 = _lastValidValue1 = _p_lastValidValue1 = 0; - _tickCount1 = 0; - - _sum2 = _p_sum2 = _p_lastInput2 = 0; - _tickCount2 = 0; - - _sampleCount = 0; + _state = default; + _p_state = default; Last = default; } } diff --git a/lib/trends/vidya/Vidya.cs b/lib/trends/vidya/Vidya.cs index cdc99788..f8c50069 100644 --- a/lib/trends/vidya/Vidya.cs +++ b/lib/trends/vidya/Vidya.cs @@ -31,12 +31,13 @@ public sealed class Vidya : ITValuePublisher private readonly RingBuffer _ups; private readonly RingBuffer _downs; - private double _prevClose; - private double _lastVidya; - private double _currentClose; - private double _currentVidya; - private bool _isInitialized; - private int _barCount; + private record struct State( + double PrevClose, double LastVidya, + double CurrentClose, double CurrentVidya, + bool IsInitialized, int BarCount + ); + private State _state; + private State _p_state; /// /// Display name for the indicator. @@ -78,12 +79,18 @@ public sealed class Vidya : ITValuePublisher { if (isNew) { - _barCount++; - if (_isInitialized) - { - _prevClose = _currentClose; - _lastVidya = _currentVidya; - } + _p_state = _state; + } + else + { + _state = _p_state; + } + + _state.BarCount++; + if (_state.IsInitialized) + { + _state.PrevClose = _state.CurrentClose; + _state.LastVidya = _state.CurrentVidya; } double price = input.Value; @@ -91,25 +98,25 @@ public sealed class Vidya : ITValuePublisher { // Handle NaN/Infinity by using the last known valid values // If not initialized, we can't do much, just return input - if (!_isInitialized) return input; - price = _currentClose; // Use last valid close + if (!_state.IsInitialized) return input; + price = _state.CurrentClose; // Use last valid close } - if (_barCount <= 1) + if (_state.BarCount <= 1) { - _prevClose = price; - _lastVidya = price; - _currentClose = price; - _currentVidya = price; - _isInitialized = true; + _state.PrevClose = price; + _state.LastVidya = price; + _state.CurrentClose = price; + _state.CurrentVidya = price; + _state.IsInitialized = true; _ups.Add(0, isNew); _downs.Add(0, isNew); - Last = new TValue(input.Time, _currentVidya); + Last = new TValue(input.Time, _state.CurrentVidya); Pub?.Invoke(Last); return Last; } - double change = price - _prevClose; + double change = price - _state.PrevClose; double up = change > 0 ? change : 0; double down = change < 0 ? -change : 0; @@ -127,10 +134,10 @@ public sealed class Vidya : ITValuePublisher } double dynamicAlpha = _alpha * vi; - _currentVidya = dynamicAlpha * price + (1.0 - dynamicAlpha) * _lastVidya; - _currentClose = price; + _state.CurrentVidya = dynamicAlpha * price + (1.0 - dynamicAlpha) * _state.LastVidya; + _state.CurrentClose = price; - Last = new TValue(input.Time, _currentVidya); + Last = new TValue(input.Time, _state.CurrentVidya); Pub?.Invoke(Last); return Last; } @@ -150,42 +157,10 @@ public sealed class Vidya : ITValuePublisher var sourceValues = source.Values; var sourceTimes = source.Times; - // We can't easily use a static Calculate here because of the complex state (RingBuffers) - // So we'll iterate and use the instance Update logic, but optimized for series - // Actually, we can implement a static Calculate that uses temporary buffers - Calculate(sourceValues, vSpan, _period); sourceTimes.CopyTo(tSpan); - // Update internal state to match the end of the series - // This is tricky because Calculate is static and doesn't update instance state. - // To support "Update(TSeries)", we should probably just run the instance update loop. - // But for performance, we want to use the static method if possible. - // The standard pattern in this library seems to be: - // 1. Call static Calculate to fill the output - // 2. Re-run the last N updates on the instance to sync state - - // Re-sync state - // We need to feed at least 'period' bars to fill the buffers - // But since VIDYA is recursive, we really need the whole history to match exactly. - // So for VIDYA, it's safer to just reset and run the update loop. - - Reset(); - for (int i = 0; i < len; i++) - { - Update(new TValue(sourceTimes[i], sourceValues[i]), true); - } - - // Overwrite the vSpan with the results we just calculated? - // Or just trust the loop we just ran. - // Since we ran the loop, 'v' is already populated? No, Update(TValue) updates 'Last', not a list. - // So we need to populate 'v'. - - // Let's do this: - // 1. Reset - // 2. Loop and populate - Reset(); for (int i = 0; i < len; i++) { @@ -210,14 +185,6 @@ public sealed class Vidya : ITValuePublisher double alpha = 2.0 / (period + 1); - // We need buffers for Up and Down sums - // Since we can't allocate RingBuffers on the stack easily for dynamic period, - // and we want to avoid heap allocations in the hot path if possible. - // But for a static Calculate with a large span, a few allocations are acceptable. - // Or we can use a circular buffer logic with a stackalloc array if period is small, - // but period can be large. - - // Let's use a simple array for the circular buffer logic double[] ups = new double[period]; double[] downs = new double[period]; int head = 0; @@ -227,11 +194,8 @@ public sealed class Vidya : ITValuePublisher double prevClose = source[0]; double lastVidya = source[0]; - // Initialize first element output[0] = source[0]; - // Fill buffers with 0 initially (already done by new double[]) - for (int i = 1; i < source.Length; i++) { double price = source[i]; @@ -244,7 +208,6 @@ public sealed class Vidya : ITValuePublisher double up = change > 0 ? change : 0; double down = change < 0 ? -change : 0; - // Update sums: remove old, add new sumUp -= ups[head]; sumDown -= downs[head]; @@ -277,13 +240,8 @@ public sealed class Vidya : ITValuePublisher { _ups.Clear(); _downs.Clear(); - _prevClose = 0; - _lastVidya = 0; - _currentClose = 0; - _currentVidya = 0; - _isInitialized = false; - _barCount = 0; - + _state = default; + _p_state = default; Last = default; } } diff --git a/lib/trends/wma/Wma.cs b/lib/trends/wma/Wma.cs index 395567ff..2d9c1d6b 100644 --- a/lib/trends/wma/Wma.cs +++ b/lib/trends/wma/Wma.cs @@ -32,10 +32,10 @@ public sealed class Wma : ITValuePublisher private readonly double _divisor; private readonly RingBuffer _buffer; - private double _sum, _wsum; - private double _p_sum, _p_wsum, _p_lastInput; - private double _lastValidValue, _p_lastValidValue; - private int _tickCount; + private record struct State(double Sum, double WSum, double LastInput, double LastValidValue, int TickCount); + private State _state; + private State _p_state; + private const int ResyncInterval = 1000; public string Name { get; } @@ -63,10 +63,10 @@ public sealed class Wma : ITValuePublisher { if (double.IsFinite(input)) { - _lastValidValue = input; + _state.LastValidValue = input; return input; } - return _lastValidValue; + return _state.LastValidValue; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -74,24 +74,24 @@ public sealed class Wma : ITValuePublisher { if (_buffer.IsFull) { - double oldSum = _sum; + double oldSum = _state.Sum; double oldest = _buffer.Oldest; - _sum = _sum - oldest + val; - _wsum = _wsum - oldSum + (_period * val); + _state.Sum = _state.Sum - oldest + val; + _state.WSum = _state.WSum - oldSum + (_period * val); } else { int count = _buffer.Count + 1; - _sum += val; - _wsum += count * val; + _state.Sum += val; + _state.WSum += count * val; } _buffer.Add(val); - _tickCount++; - if (_buffer.IsFull && _tickCount >= ResyncInterval) + _state.TickCount++; + if (_buffer.IsFull && _state.TickCount >= ResyncInterval) { - _tickCount = 0; + _state.TickCount = 0; double recalcSum = 0; double recalcWsum = 0; int weight = 1; @@ -101,8 +101,8 @@ public sealed class Wma : ITValuePublisher recalcWsum += weight * item; weight++; } - _sum = recalcSum; - _wsum = recalcWsum; + _state.Sum = recalcSum; + _state.WSum = recalcWsum; } } @@ -113,29 +113,24 @@ public sealed class Wma : ITValuePublisher { double val = GetValidValue(input.Value); UpdateState(val); + _state.LastInput = val; - _p_sum = _sum; - _p_wsum = _wsum; - _p_lastInput = val; - _p_lastValidValue = _lastValidValue; + _p_state = _state; } else { - _lastValidValue = _p_lastValidValue; + _state = _p_state; double val = GetValidValue(input.Value); - _sum = _p_sum; - _wsum = _p_wsum; - int weight = _buffer.IsFull ? _period : _buffer.Count; - _sum = _sum - _p_lastInput + val; - _wsum += weight * (val - _p_lastInput); + _state.Sum = _state.Sum - _state.LastInput + val; + _state.WSum += weight * (val - _state.LastInput); _buffer.UpdateNewest(val); } double currentDivisor = _buffer.IsFull ? _divisor : (double)_buffer.Count * (_buffer.Count + 1) * 0.5; - Last = new TValue(input.Time, _wsum / currentDivisor); + Last = new TValue(input.Time, _state.WSum / currentDivisor); Pub?.Invoke(Last); return Last; } @@ -162,35 +157,34 @@ public sealed class Wma : ITValuePublisher if (startIndex > 0) { + _state.LastValidValue = 0; for (int i = startIndex - 1; i >= 0; i--) { if (double.IsFinite(source.Values[i])) { - _lastValidValue = source.Values[i]; + _state.LastValidValue = source.Values[i]; break; } } } else { - _lastValidValue = 0; + _state.LastValidValue = 0; } _buffer.Clear(); - _sum = 0; - _wsum = 0; - _tickCount = 0; + _state.Sum = 0; + _state.WSum = 0; + _state.TickCount = 0; for (int i = startIndex; i < len; i++) { double val = GetValidValue(source.Values[i]); UpdateState(val); + _state.LastInput = val; } - _p_sum = _sum; - _p_wsum = _wsum; - _p_lastInput = GetValidValue(source.Values[len - 1]); - _p_lastValidValue = _lastValidValue; + _p_state = _state; Last = new TValue(tSpan[len - 1], vSpan[len - 1]); return new TSeries(t, v); @@ -754,7 +748,8 @@ public sealed class Wma : ITValuePublisher public void Reset() { _buffer.Clear(); - _sum = _wsum = _p_sum = _p_wsum = _p_lastInput = _lastValidValue = _p_lastValidValue = 0; + _state = default; + _p_state = default; Last = default; } } diff --git a/test_record_struct.cs b/test_record_struct.cs new file mode 100644 index 00000000..dcaf5c5a --- /dev/null +++ b/test_record_struct.cs @@ -0,0 +1,23 @@ +using System; + +namespace QuanTAlib; + +public class RecordStructTest +{ + private record struct State(double Val, bool Flag); + + public static void Run() + { + var s1 = new State(1.0, true); + var s2 = new State(1.0, true); + var s3 = new State(2.0, false); + + Console.WriteLine($"s1 == s2: {s1 == s2}"); // Should be True + Console.WriteLine($"s1 == s3: {s1 == s3}"); // Should be False + Console.WriteLine($"s1 equals s2: {s1.Equals(s2)}"); // Should be True + + // Verify mutability + s1.Val = 3.0; + Console.WriteLine($"s1 modified: {s1.Val}"); + } +}