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}");
+ }
+}