Refactor trend indicators to use record structs for state management

This commit is contained in:
Miha Kralj
2025-12-10 21:58:45 -05:00
parent cfc54bf1f7
commit e6033638ad
17 changed files with 390 additions and 484 deletions
+15 -7
View File
@@ -39,8 +39,9 @@ Each indicator resides in its own directory such as `lib/trends/`, `lib/indicato
### State Management ### State Management
* Use `RingBuffer` for sliding window data. * **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.
* Maintain separate state variables for the *current* calculation (`_sum`, `_lastVal`) and the *previous* valid state (`_p_sum`, `_p_lastVal`) to support `isNew=false` updates. * **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. * **Resync:** Implement a periodic full recalculation (e.g., every 1000 ticks) to prevent floating-point drift in running sums.
### Constructor ### 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)` * **Signature:** `public TValue Update(TValue input, bool isNew = true)`
* **Attribute:** `[MethodImpl(MethodImplOptions.AggressiveInlining)]` * **Attribute:** `[MethodImpl(MethodImplOptions.AggressiveInlining)]`
* **Logic:** * **Logic:**
1. **Input Validation:** Check `double.IsFinite`. If not, use `_lastValidValue`. 1. **State Rollback:**
2. **State Management:** ```csharp
* If `isNew=true`: Save current state to `_p_*` variables, then update. if (isNew) {
* If `isNew=false`: Restore state from `_p_*` variables, then update. _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. 3. **Calculation:** Perform the math.
4. **Publish:** Update `Last` property, invoke `Pub` event, return `Last`. 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:** * **Logic:**
1. Create output series. 1. Create output series.
2. Call static `Calculate(Span)` for performance. 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) ### Static Calculate (TSeries)
+4 -3
View File
@@ -65,9 +65,10 @@ public TValue Update(TValue input, bool isNew = true)
### State Management ### State Management
* Use `RingBuffer` for sliding windows. * **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.
* Maintain `_state` and `_p_state` (previous state) variables to support `isNew=false` rollbacks. * **State Variables:** Maintain `private State _state;` (current) and `private State _p_state;` (previous valid state).
* **Resync**: Periodically recalculate running sums to prevent floating-point drift. * **Buffers:** Use `RingBuffer` for sliding windows.
* **Resync:** Periodically recalculate running sums to prevent floating-point drift.
### Dual API Requirement ### Dual API Requirement
+29 -11
View File
@@ -1,4 +1,5 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Security.Cryptography;
namespace QuanTAlib; namespace QuanTAlib;
@@ -13,7 +14,7 @@ namespace QuanTAlib;
public class GBM : IFeed public class GBM : IFeed
#pragma warning restore S101 #pragma warning restore S101
{ {
private readonly Random _rnd; private readonly Random? _rnd;
private double _lastPrice; private double _lastPrice;
private long _lastTime; private long _lastTime;
@@ -52,7 +53,7 @@ public class GBM : IFeed
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(startPrice); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(startPrice);
ArgumentOutOfRangeException.ThrowIfNegative(sigma); ArgumentOutOfRangeException.ThrowIfNegative(sigma);
_rnd = seed.HasValue ? new Random(seed.Value) : new Random(); _rnd = seed.HasValue ? new Random(seed.Value) : null;
_lastPrice = startPrice; _lastPrice = startPrice;
_lastTime = DateTime.UtcNow.Ticks; _lastTime = DateTime.UtcNow.Ticks;
@@ -71,6 +72,23 @@ public class GBM : IFeed
_vol = sigma * Math.Sqrt(dt); _vol = sigma * Math.Sqrt(dt);
} }
/// <summary>
/// Generates a random double in [0, 1) using either the seeded Random or RandomNumberGenerator.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double NextDouble()
{
if (_rnd != null)
{
return _rnd.NextDouble();
}
Span<byte> buffer = stackalloc byte[8];
RandomNumberGenerator.Fill(buffer);
ulong ul = BitConverter.ToUInt64(buffer);
return (ul >> 11) * (1.0 / (1ul << 53));
}
/// <summary> /// <summary>
/// Generates next standard normal using Box-Muller transform with caching. /// Generates next standard normal using Box-Muller transform with caching.
/// </summary> /// </summary>
@@ -83,8 +101,8 @@ public class GBM : IFeed
return _cachedZ; return _cachedZ;
} }
double u1 = 1.0 - _rnd.NextDouble(); // nosemgrep double u1 = 1.0 - NextDouble();
double u2 = 1.0 - _rnd.NextDouble(); // nosemgrep double u2 = 1.0 - NextDouble();
double mag = Math.Sqrt(-2.0 * Math.Log(u1)); double mag = Math.Sqrt(-2.0 * Math.Log(u1));
double angle = 2.0 * Math.PI * u2; double angle = 2.0 * Math.PI * u2;
@@ -110,12 +128,12 @@ public class GBM : IFeed
double z = NextNormal(); double z = NextNormal();
double price = _lastPrice * Math.Exp(_drift + _vol * z); double price = _lastPrice * Math.Exp(_drift + _vol * z);
double volume = 1000 + _rnd.NextDouble() * 1000; double volume = 1000 + NextDouble() * 1000;
double open = _lastPrice; double open = _lastPrice;
double close = price; double close = price;
double high = Math.Max(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 - _rnd.NextDouble() * 0.01); double low = Math.Min(open, close) * (1.0 - NextDouble() * 0.01);
_currentBar = new TBar(currentTime, open, high, low, close, volume); _currentBar = new TBar(currentTime, open, high, low, close, volume);
_hasCurrentBar = true; _hasCurrentBar = true;
@@ -128,7 +146,7 @@ public class GBM : IFeed
// Update current bar (intra-bar tick) // Update current bar (intra-bar tick)
double z = NextNormal(); double z = NextNormal();
double price = _lastPrice * Math.Exp(_drift + _vol * z); double price = _lastPrice * Math.Exp(_drift + _vol * z);
double additionalVolume = 1000 + _rnd.NextDouble() * 1000; double additionalVolume = 1000 + NextDouble() * 1000;
var bar = _currentBar; var bar = _currentBar;
double newClose = price; double newClose = price;
@@ -190,9 +208,9 @@ public class GBM : IFeed
double open = currentPrice; double open = currentPrice;
double close = price; double close = price;
double rnd1 = _rnd.NextDouble(); double rnd1 = NextDouble();
double rnd2 = _rnd.NextDouble(); double rnd2 = NextDouble();
double rnd3 = _rnd.NextDouble(); double rnd3 = NextDouble();
t[i] = currentTime; t[i] = currentTime;
o[i] = open; o[i] = open;
+18 -5
View File
@@ -31,7 +31,10 @@ public sealed class Alma : ITValuePublisher
private readonly double[] _weights; private readonly double[] _weights;
private readonly double _weightSum; private readonly double _weightSum;
private readonly RingBuffer _buffer; private readonly RingBuffer _buffer;
private double _lastValidValue;
private record struct State(double LastValidValue);
private State _state;
private State _p_state;
/// <summary> /// <summary>
/// Display name for the indicator. /// Display name for the indicator.
@@ -99,15 +102,24 @@ public sealed class Alma : ITValuePublisher
{ {
if (double.IsFinite(input)) if (double.IsFinite(input))
{ {
_lastValidValue = input; _state.LastValidValue = input;
return input; return input;
} }
return _lastValidValue; return _state.LastValidValue;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true) public TValue Update(TValue input, bool isNew = true)
{ {
if (isNew)
{
_p_state = _state;
}
else
{
_state = _p_state;
}
double val = GetValidValue(input.Value); double val = GetValidValue(input.Value);
_buffer.Add(val, isNew); _buffer.Add(val, isNew);
@@ -140,7 +152,7 @@ public sealed class Alma : ITValuePublisher
// Restore state // Restore state
_buffer.Clear(); _buffer.Clear();
_lastValidValue = 0; _state = default;
// Replay last part to restore buffer state // Replay last part to restore buffer state
int startIndex = Math.Max(0, len - _period); int startIndex = Math.Max(0, len - _period);
@@ -301,7 +313,8 @@ public sealed class Alma : ITValuePublisher
public void Reset() public void Reset()
{ {
_buffer.Clear(); _buffer.Clear();
_lastValidValue = 0; _state = default;
_p_state = default;
Last = default; Last = default;
} }
} }
+14 -15
View File
@@ -25,10 +25,9 @@ public sealed class Conv : ITValuePublisher
private readonly double[] _kernel; private readonly double[] _kernel;
private readonly RingBuffer _buffer; private readonly RingBuffer _buffer;
private double _lastValidValue; private record struct State(double LastValidValue);
private State _state;
// State for bar correction private State _p_state;
private double _p_lastValidValue;
public string Name { get; } public string Name { get; }
public TValue Last { get; private set; } public TValue Last { get; private set; }
@@ -45,8 +44,8 @@ public sealed class Conv : ITValuePublisher
Array.Copy(kernel, _kernel, _period); Array.Copy(kernel, _kernel, _period);
_buffer = new RingBuffer(_period); _buffer = new RingBuffer(_period);
Name = $"Conv({_period})"; Name = $"Conv({_period})";
_lastValidValue = double.NaN; _state.LastValidValue = double.NaN;
_p_lastValidValue = double.NaN; _p_state.LastValidValue = double.NaN;
} }
public Conv(ITValuePublisher source, double[] kernel) : this(kernel) public Conv(ITValuePublisher source, double[] kernel) : this(kernel)
@@ -59,10 +58,10 @@ public sealed class Conv : ITValuePublisher
{ {
if (double.IsFinite(input)) if (double.IsFinite(input))
{ {
_lastValidValue = input; _state.LastValidValue = input;
return input; return input;
} }
return _lastValidValue; return _state.LastValidValue;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -70,11 +69,11 @@ public sealed class Conv : ITValuePublisher
{ {
if (isNew) if (isNew)
{ {
_p_lastValidValue = _lastValidValue; _p_state = _state;
} }
else else
{ {
_lastValidValue = _p_lastValidValue; _state = _p_state;
} }
double val = GetValidValue(input.Value); double val = GetValidValue(input.Value);
@@ -145,14 +144,14 @@ public sealed class Conv : ITValuePublisher
{ {
if (double.IsFinite(sourceValues[i])) if (double.IsFinite(sourceValues[i]))
{ {
_lastValidValue = sourceValues[i]; _state.LastValidValue = sourceValues[i];
break; break;
} }
} }
} }
else else
{ {
_lastValidValue = double.NaN; _state.LastValidValue = double.NaN;
} }
_buffer.Clear(); _buffer.Clear();
@@ -168,7 +167,7 @@ public sealed class Conv : ITValuePublisher
Last = new TValue(source.Times[len - 1], vSpan[len - 1]); Last = new TValue(source.Times[len - 1], vSpan[len - 1]);
// Save state for isNew=false // Save state for isNew=false
_p_lastValidValue = _lastValidValue; _p_state = _state;
return new TSeries(t, v); return new TSeries(t, v);
} }
@@ -241,8 +240,8 @@ public sealed class Conv : ITValuePublisher
public void Reset() public void Reset()
{ {
_buffer.Clear(); _buffer.Clear();
_lastValidValue = double.NaN; _state.LastValidValue = double.NaN;
_p_lastValidValue = double.NaN; _p_state.LastValidValue = double.NaN;
Last = default; Last = default;
} }
} }
+1 -20
View File
@@ -24,28 +24,9 @@ namespace QuanTAlib;
[SkipLocalsInit] [SkipLocalsInit]
public sealed class Dema : ITValuePublisher public sealed class Dema : ITValuePublisher
{ {
private struct EmaState : IEquatable<EmaState> 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 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; private readonly double _alpha;
+1 -20
View File
@@ -27,28 +27,9 @@ namespace QuanTAlib;
[SkipLocalsInit] [SkipLocalsInit]
public sealed class Ema : ITValuePublisher public sealed class Ema : ITValuePublisher
{ {
private struct State : IEquatable<State> 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 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; private readonly double _alpha;
+52 -37
View File
@@ -24,12 +24,10 @@ public sealed class Kama : ITValuePublisher
private readonly double _fastAlpha; private readonly double _fastAlpha;
private readonly double _slowAlpha; private readonly double _slowAlpha;
private readonly RingBuffer _buffer; private readonly RingBuffer _buffer;
private double _kama;
private double _p_kama; private record struct State(double Kama, double VolatilitySum, double NextDiffOut, double LastValidValue);
private double _volatilitySum; private State _state;
private double _p_volatilitySum; private State _p_state;
private double _lastDiffOut;
private double _lastValidValue;
/// <summary> /// <summary>
/// Display name for the indicator. /// Display name for the indicator.
@@ -74,8 +72,10 @@ public sealed class Kama : ITValuePublisher
_slowAlpha = 2.0 / (slowPeriod + 1); _slowAlpha = 2.0 / (slowPeriod + 1);
Name = $"Kama({period}, {fastPeriod}, {slowPeriod})"; Name = $"Kama({period}, {fastPeriod}, {slowPeriod})";
_kama = double.NaN; _state.Kama = double.NaN;
_lastValidValue = 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) 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)) if (double.IsFinite(input))
{ {
_lastValidValue = input; _state.LastValidValue = input;
return input; return input;
} }
return _lastValidValue; return _state.LastValidValue;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true) public TValue Update(TValue input, bool isNew = true)
{ {
if (isNew)
{
_p_state = _state;
}
else
{
_state = _p_state;
}
double val = GetValidValue(input.Value); double val = GetValidValue(input.Value);
if (double.IsNaN(val)) if (double.IsNaN(val))
{ {
@@ -108,55 +117,58 @@ public sealed class Kama : ITValuePublisher
if (isNew) if (isNew)
{ {
_p_kama = _kama;
_p_volatilitySum = _volatilitySum;
bool wasFull = _buffer.IsFull; bool wasFull = _buffer.IsFull;
double removed = _buffer.Add(val); _buffer.Add(val);
if (wasFull) if (wasFull)
{ {
double diff_out = Math.Abs(removed - _buffer[0]); double diff_out = _p_state.NextDiffOut;
_lastDiffOut = diff_out;
double diff_in = Math.Abs(_buffer[^1] - _buffer[^2]); 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) else if (_buffer.Count >= 2)
{ {
double diff_in = Math.Abs(_buffer[^1] - _buffer[^2]); double diff_in = Math.Abs(_buffer[^1] - _buffer[^2]);
_volatilitySum += diff_in; _state.VolatilitySum += diff_in;
_lastDiffOut = 0;
if (_buffer.IsFull)
{
// Buffer just became full.
// NextDiffOut = abs(buffer[0] - buffer[1])
_state.NextDiffOut = Math.Abs(_buffer[0] - _buffer[1]);
}
} }
} }
else else
{ {
// Restore state
_kama = _p_kama;
_buffer.UpdateNewest(val); _buffer.UpdateNewest(val);
if (_buffer.IsFull) if (_buffer.IsFull)
{ {
double diff_in = Math.Abs(_buffer[^1] - _buffer[^2]); 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) else if (_buffer.Count >= 2)
{ {
double diff_in = Math.Abs(_buffer[^1] - _buffer[^2]); double diff_in = Math.Abs(_buffer[^1] - _buffer[^2]);
_volatilitySum = _p_volatilitySum + diff_in; _state.VolatilitySum = _p_state.VolatilitySum + diff_in;
} }
} }
// Calculate KAMA // Calculate KAMA
if (double.IsNaN(_kama)) if (double.IsNaN(_state.Kama))
{ {
_kama = val; _state.Kama = val;
_p_kama = val; // Ensure p_kama is initialized
} }
else else
{ {
double change = Math.Abs(_buffer[^1] - _buffer[0]); double change = Math.Abs(_buffer[^1] - _buffer[0]);
double volatility = _volatilitySum; double volatility = _state.VolatilitySum;
// Avoid division by zero // Avoid division by zero
double er = (volatility > double.Epsilon) ? change / volatility : 0.0; double er = (volatility > double.Epsilon) ? change / volatility : 0.0;
@@ -166,10 +178,16 @@ public sealed class Kama : ITValuePublisher
double sc = er * (_fastAlpha - _slowAlpha) + _slowAlpha; double sc = er * (_fastAlpha - _slowAlpha) + _slowAlpha;
sc *= sc; 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); Pub?.Invoke(Last);
return Last; return Last;
} }
@@ -290,7 +308,6 @@ public sealed class Kama : ITValuePublisher
double change = 0; double change = 0;
change = (count == bufSize) ? Math.Abs(val - buffer[bufferIdx]) : Math.Abs(val - buffer[0]); change = (count == bufSize) ? Math.Abs(val - buffer[bufferIdx]) : Math.Abs(val - buffer[0]);
double er = (volatilitySum > double.Epsilon) ? change / volatilitySum : 0.0; double er = (volatilitySum > double.Epsilon) ? change / volatilitySum : 0.0;
if (er > 1.0) er = 1.0; if (er > 1.0) er = 1.0;
@@ -306,12 +323,10 @@ public sealed class Kama : ITValuePublisher
public void Reset() public void Reset()
{ {
_buffer.Clear(); _buffer.Clear();
_kama = double.NaN; _state = default;
_p_kama = double.NaN; _state.Kama = double.NaN;
_volatilitySum = 0; _state.LastValidValue = double.NaN;
_p_volatilitySum = 0; _p_state = _state;
_lastDiffOut = 0;
_lastValidValue = double.NaN;
Last = default; Last = default;
} }
} }
+29 -43
View File
@@ -34,15 +34,10 @@ public sealed class Lsma : ITValuePublisher
private readonly double _sum_x; private readonly double _sum_x;
private readonly double _denominator; private readonly double _denominator;
private double _sum_y; private record struct State(double SumY, double SumXY, double LastVal, double LastValidValue);
private double _sum_xy; 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 int _tickCount;
private const int ResyncInterval = 1000; private const int ResyncInterval = 1000;
@@ -100,10 +95,10 @@ public sealed class Lsma : ITValuePublisher
{ {
if (double.IsFinite(input)) if (double.IsFinite(input))
{ {
_lastValidValue = input; _state.LastValidValue = input;
return input; return input;
} }
return _lastValidValue; return _state.LastValidValue;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -112,24 +107,24 @@ public sealed class Lsma : ITValuePublisher
if (_buffer.IsFull) if (_buffer.IsFull)
{ {
double oldest = _buffer.Oldest; double oldest = _buffer.Oldest;
double prev_sum_y = _sum_y; double prev_sum_y = _state.SumY;
// O(1) update for sum_xy // O(1) update for sum_xy
// sum_xy_new = sum_xy_old + sum_y_prev - n * oldest // 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 // O(1) update for sum_y
_sum_y = _sum_y - oldest + val; _state.SumY = _state.SumY - oldest + val;
_buffer.Add(val); _buffer.Add(val);
} }
else else
{ {
_buffer.Add(val); _buffer.Add(val);
_sum_y += val; _state.SumY += val;
// Recalculate sum_xy from scratch during warmup // Recalculate sum_xy from scratch during warmup
_sum_xy = 0; _state.SumXY = 0;
var span = _buffer.GetSpan(); var span = _buffer.GetSpan();
for (int i = 0; i < span.Length; i++) 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 // index j in buffer corresponds to x = count - 1 - j
// sum_xy = sum(x * y) // sum_xy = sum(x * y)
int x = span.Length - 1 - i; 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() private void Resync()
{ {
_sum_y = _buffer.Sum; _state.SumY = _buffer.Sum;
_sum_xy = 0; _state.SumXY = 0;
var span = _buffer.GetSpan(); var span = _buffer.GetSpan();
for (int i = 0; i < span.Length; i++) for (int i = 0; i < span.Length; i++)
{ {
int x = span.Length - 1 - 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); double val = GetValidValue(input.Value);
UpdateState(val); UpdateState(val);
_p_sum_y = _sum_y; _p_state = _state;
_p_sum_xy = _sum_xy; _state.LastVal = val;
_p_last_val = val;
_p_lastValidValue = _lastValidValue;
} }
else else
{ {
_lastValidValue = _p_lastValidValue; _state.LastValidValue = _p_state.LastValidValue;
double val = GetValidValue(input.Value); double val = GetValidValue(input.Value);
// For isNew=false, we update the current bar. // 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_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 updates to reflect the change in the newest value.
_sum_y = _p_sum_y - _p_last_val + val; _state.SumY = _p_state.SumY - _p_state.LastVal + val;
_sum_xy = _p_sum_xy; // Restore sum_xy to the state after the shift _state.SumXY = _p_state.SumXY; // Restore sum_xy to the state after the shift
_buffer.UpdateNewest(val); _buffer.UpdateNewest(val);
_p_last_val = val; _state.LastVal = val;
} }
double result; double result;
@@ -218,8 +211,8 @@ public sealed class Lsma : ITValuePublisher
} }
else else
{ {
double m = (n * _sum_xy - sx * _sum_y) / denom; double m = (n * _state.SumXY - sx * _state.SumY) / denom;
double b = (_sum_y - m * sx) / n; double b = (_state.SumY - m * sx) / n;
// LSMA = b - m * offset // LSMA = b - m * offset
result = b - m * _offset; result = b - m * _offset;
@@ -261,17 +254,17 @@ public sealed class Lsma : ITValuePublisher
{ {
if (double.IsFinite(source.Values[i])) if (double.IsFinite(source.Values[i]))
{ {
_lastValidValue = source.Values[i]; _state.LastValidValue = source.Values[i];
break; break;
} }
} }
} }
else else
{ {
_lastValidValue = 0; _state.LastValidValue = 0;
} }
double lastProcessedValue = _lastValidValue; double lastProcessedValue = _state.LastValidValue;
for (int i = startIndex; i < len; i++) for (int i = startIndex; i < len; i++)
{ {
double val = GetValidValue(source.Values[i]); double val = GetValidValue(source.Values[i]);
@@ -279,10 +272,8 @@ public sealed class Lsma : ITValuePublisher
lastProcessedValue = val; lastProcessedValue = val;
} }
_p_sum_y = _sum_y; _state.LastVal = lastProcessedValue;
_p_sum_xy = _sum_xy; _p_state = _state;
_p_last_val = lastProcessedValue;
_p_lastValidValue = _lastValidValue;
Last = new TValue(tSpan[len - 1], vSpan[len - 1]); Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
return new TSeries(t, v); return new TSeries(t, v);
@@ -409,14 +400,9 @@ public sealed class Lsma : ITValuePublisher
public void Reset() public void Reset()
{ {
_buffer.Clear(); _buffer.Clear();
_sum_y = 0; _state = default;
_sum_xy = 0; _p_state = default;
_p_sum_y = 0;
_p_sum_xy = 0;
_p_last_val = 0;
Last = default; Last = default;
_tickCount = 0; _tickCount = 0;
_lastValidValue = 0;
_p_lastValidValue = 0;
} }
} }
+39 -70
View File
@@ -12,25 +12,18 @@ public sealed class Mama : ITValuePublisher
{ {
public TValue Last { get; private set; } public TValue Last { get; private set; }
public TValue Fama { get; private set; } public TValue Fama { get; private set; }
public bool IsHot => _index > 6; public bool IsHot => _state.Index > 6;
public event Action<TValue>? Pub; public event Action<TValue>? Pub;
private readonly double _fastLimit; private readonly double _fastLimit;
private readonly double _slowLimit; private readonly double _slowLimit;
private double _period, _p_period; private record struct State(
private double _phase, _p_phase; double Period, double Phase, double Mama, double Fama, double SumPr,
private double _mama, _p_mama; double I2, double Q2, double Re, double Im, double LastValidPrice, int Index
private double _fama, _p_fama; );
private double _sumPr, _p_sumPr; private State _state;
private int _index; private State _p_state;
// 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 readonly RingBuffer _priceBuffer; private readonly RingBuffer _priceBuffer;
private readonly RingBuffer _smoothBuffer; private readonly RingBuffer _smoothBuffer;
@@ -69,18 +62,10 @@ public sealed class Mama : ITValuePublisher
public void Init() public void Init()
{ {
_period = _p_period = 0.0; _state = default;
_phase = _p_phase = 0.0; _state.Mama = double.NaN;
_mama = _p_mama = double.NaN; _state.Fama = double.NaN;
_fama = _p_fama = double.NaN; _p_state = _state;
_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;
_priceBuffer.Clear(); _priceBuffer.Clear();
_smoothBuffer.Clear(); _smoothBuffer.Clear();
@@ -97,45 +82,29 @@ public sealed class Mama : ITValuePublisher
{ {
if (isNew) if (isNew)
{ {
_p_period = _period; _p_state = _state;
_p_phase = _phase; _state.Index++;
_p_mama = _mama;
_p_fama = _fama;
_p_sumPr = _sumPr;
_p_i2 = _i2;
_p_q2 = _q2;
_p_re = _re;
_p_im = _im;
_index++;
} }
else else
{ {
_period = _p_period; _state = _p_state;
_phase = _p_phase;
_mama = _p_mama;
_fama = _p_fama;
_sumPr = _p_sumPr;
_i2 = _p_i2;
_q2 = _p_q2;
_re = _p_re;
_im = _p_im;
} }
double price = input.Value; double price = input.Value;
if (!double.IsFinite(price)) if (!double.IsFinite(price))
{ {
price = _lastValidPrice; price = _state.LastValidPrice;
} }
else else
{ {
_lastValidPrice = price; _state.LastValidPrice = price;
} }
_priceBuffer.Add(price, isNew); _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 // Smooth
double smooth = (4.0 * _priceBuffer[0] + 3.0 * _priceBuffer[1] + 2.0 * _priceBuffer[2] + _priceBuffer[3]) * 0.1; 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; double q2_val = q1 + jI;
// Smooth i2, q2 // Smooth i2, q2
_i2 = 0.2 * i2_val + 0.8 * _p_i2; _state.I2 = 0.2 * i2_val + 0.8 * _p_state.I2;
_q2 = 0.2 * q2_val + 0.8 * _p_q2; _state.Q2 = 0.2 * q2_val + 0.8 * _p_state.Q2;
// Homodyne discriminator // Homodyne discriminator
double re_val = (_i2 * _p_i2) + (_q2 * _p_q2); double re_val = (_state.I2 * _p_state.I2) + (_state.Q2 * _p_state.Q2);
double im_val = (_i2 * _p_q2) - (_q2 * _p_i2); double im_val = (_state.I2 * _p_state.Q2) - (_state.Q2 * _p_state.I2);
// Smooth re, im // Smooth re, im
_re = 0.2 * re_val + 0.8 * _p_re; _state.Re = 0.2 * re_val + 0.8 * _p_state.Re;
_im = 0.2 * im_val + 0.8 * _p_im; _state.Im = 0.2 * im_val + 0.8 * _p_state.Im;
// Calculate Period // Calculate Period
double period = (Math.Abs(_im) > double.Epsilon && Math.Abs(_re) > double.Epsilon) double period = (Math.Abs(_state.Im) > double.Epsilon && Math.Abs(_state.Re) > double.Epsilon)
? TWOPI / Math.Atan(_im / _re) ? TWOPI / Math.Atan(_state.Im / _state.Re)
: 0.0; : 0.0;
// Adjust Period // Adjust Period
period = period > 1.5 * _p_period ? 1.5 * _p_period : period; period = period > 1.5 * _p_state.Period ? 1.5 * _p_state.Period : period;
period = period < 0.67 * _p_period ? 0.67 * _p_period : period; period = period < 0.67 * _p_state.Period ? 0.67 * _p_state.Period : period;
period = period < 6.0 ? 6.0 : period; period = period < 6.0 ? 6.0 : period;
period = period > 50.0 ? 50.0 : period; period = period > 50.0 ? 50.0 : period;
// Smooth Period // Smooth Period
_period = 0.2 * period + 0.8 * _p_period; _state.Period = 0.2 * period + 0.8 * _p_state.Period;
// Phase calculation // 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 // 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; double alpha = _fastLimit / delta;
alpha = Math.Clamp(alpha, _slowLimit, _fastLimit); alpha = Math.Clamp(alpha, _slowLimit, _fastLimit);
// Final indicators // Final indicators
_mama = alpha * _priceBuffer[0] + (1.0 - alpha) * _p_mama; _state.Mama = alpha * _priceBuffer[0] + (1.0 - alpha) * _p_state.Mama;
_fama = 0.5 * alpha * _mama + (1.0 - 0.5 * alpha) * _p_fama; _state.Fama = 0.5 * alpha * _state.Mama + (1.0 - 0.5 * alpha) * _p_state.Fama;
} }
else else
{ {
// Initialization phase // Initialization phase
_sumPr += price; _state.SumPr += price;
double avg = _index > 0 ? _sumPr / _index : price; double avg = _state.Index > 0 ? _state.SumPr / _state.Index : price;
_mama = avg; _state.Mama = avg;
_fama = avg; _state.Fama = avg;
// Initialize buffers with 0 // Initialize buffers with 0
_smoothBuffer.Add(0, isNew); _smoothBuffer.Add(0, isNew);
@@ -216,8 +185,8 @@ public sealed class Mama : ITValuePublisher
_Q1_buffer.Add(0, isNew); _Q1_buffer.Add(0, isNew);
} }
Last = new TValue(input.Time, _mama); Last = new TValue(input.Time, _state.Mama);
Fama = new TValue(input.Time, _fama); Fama = new TValue(input.Time, _state.Fama);
Pub?.Invoke(Last); Pub?.Invoke(Last);
return Last; return Last;
} }
+24 -34
View File
@@ -31,12 +31,9 @@ public sealed class Sma : ITValuePublisher
private readonly int _period; private readonly int _period;
private readonly RingBuffer _buffer; private readonly RingBuffer _buffer;
private double _sum; private record struct State(double Sum, double LastInput, double LastValidValue, int TickCount);
private double _p_sum; private State _state;
private double _p_lastInput; private State _p_state;
private double _lastValidValue;
private double _p_lastValidValue;
private int _tickCount;
private const int ResyncInterval = 1000; private const int ResyncInterval = 1000;
@@ -85,10 +82,10 @@ public sealed class Sma : ITValuePublisher
{ {
if (double.IsFinite(input)) if (double.IsFinite(input))
{ {
_lastValidValue = input; _state.LastValidValue = input;
return input; return input;
} }
return _lastValidValue; return _state.LastValidValue;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -96,19 +93,18 @@ public sealed class Sma : ITValuePublisher
{ {
double removedValue = _buffer.Count == _buffer.Capacity ? _buffer.Oldest : 0.0; double removedValue = _buffer.Count == _buffer.Capacity ? _buffer.Oldest : 0.0;
_sum = _sum - removedValue + val; _state.Sum = _state.Sum - removedValue + val;
_buffer.Add(val); _buffer.Add(val);
_tickCount++; _state.TickCount++;
if (_buffer.IsFull && _tickCount >= ResyncInterval) if (_buffer.IsFull && _state.TickCount >= ResyncInterval)
{ {
_tickCount = 0; _state.TickCount = 0;
_sum = _buffer.RecalculateSum(); _state.Sum = _buffer.RecalculateSum();
} }
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true) public TValue Update(TValue input, bool isNew = true)
{ {
@@ -116,21 +112,20 @@ public sealed class Sma : ITValuePublisher
{ {
double val = GetValidValue(input.Value); double val = GetValidValue(input.Value);
UpdateState(val); UpdateState(val);
_state.LastInput = val;
_p_sum = _sum; _p_state = _state;
_p_lastInput = val;
_p_lastValidValue = _lastValidValue;
} }
else else
{ {
_lastValidValue = _p_lastValidValue; _state = _p_state;
double val = GetValidValue(input.Value); double val = GetValidValue(input.Value);
_sum = _p_sum - _p_lastInput + val; _state.Sum = _state.Sum - _state.LastInput + val;
_buffer.UpdateNewest(val); _buffer.UpdateNewest(val);
} }
double result = _sum / _buffer.Count; double result = _state.Sum / _buffer.Count;
Last = new TValue(input.Time, result); Last = new TValue(input.Time, result);
Pub?.Invoke(Last); Pub?.Invoke(Last);
return Last; return Last;
@@ -158,34 +153,33 @@ public sealed class Sma : ITValuePublisher
if (startIndex > 0) if (startIndex > 0)
{ {
_lastValidValue = 0; _state.LastValidValue = 0;
for (int i = startIndex - 1; i >= 0; i--) for (int i = startIndex - 1; i >= 0; i--)
{ {
if (double.IsFinite(source.Values[i])) if (double.IsFinite(source.Values[i]))
{ {
_lastValidValue = source.Values[i]; _state.LastValidValue = source.Values[i];
break; break;
} }
} }
} }
else else
{ {
_lastValidValue = 0; _state.LastValidValue = 0;
} }
_buffer.Clear(); _buffer.Clear();
_sum = 0; _state.Sum = 0;
_tickCount = 0; _state.TickCount = 0;
for (int i = startIndex; i < len; i++) for (int i = startIndex; i < len; i++)
{ {
double val = GetValidValue(source.Values[i]); double val = GetValidValue(source.Values[i]);
UpdateState(val); UpdateState(val);
_state.LastInput = val;
} }
_p_sum = _sum; _p_state = _state;
_p_lastInput = GetValidValue(source.Values[len - 1]);
_p_lastValidValue = _lastValidValue;
Last = new TValue(tSpan[len - 1], vSpan[len - 1]); Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
return new TSeries(t, v); return new TSeries(t, v);
@@ -528,12 +522,8 @@ public sealed class Sma : ITValuePublisher
public void Reset() public void Reset()
{ {
_buffer.Clear(); _buffer.Clear();
_sum = 0; _state = default;
_p_sum = 0; _p_state = default;
_p_lastInput = 0;
_lastValidValue = 0;
_p_lastValidValue = 0;
_tickCount = 0;
Last = default; Last = default;
} }
} }
+1 -19
View File
@@ -26,27 +26,9 @@ namespace QuanTAlib;
[SkipLocalsInit] [SkipLocalsInit]
public sealed class T3 : ITValuePublisher public sealed class T3 : ITValuePublisher
{ {
private struct State : IEquatable<State> 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 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<Parameters> private readonly struct Parameters : IEquatable<Parameters>
+1 -20
View File
@@ -26,28 +26,9 @@ namespace QuanTAlib;
[SkipLocalsInit] [SkipLocalsInit]
public sealed class Tema : ITValuePublisher public sealed class Tema : ITValuePublisher
{ {
private struct EmaState : IEquatable<EmaState> 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 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; private readonly double _alpha;
+73 -67
View File
@@ -31,18 +31,19 @@ public sealed class Trima : ITValuePublisher
private readonly RingBuffer _buffer1; private readonly RingBuffer _buffer1;
private readonly RingBuffer _buffer2; private readonly RingBuffer _buffer2;
private double _sum1, _p_sum1, _p_lastInput1, _lastValidValue1, _p_lastValidValue1; private record struct State(
private int _tickCount1; 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; private const int ResyncInterval = 1000;
public string Name { get; } public string Name { get; }
public TValue Last { get; private set; } public TValue Last { get; private set; }
public bool IsHot => _sampleCount >= _period; public bool IsHot => _state.SampleCount >= _period;
public event Action<TValue>? Pub; public event Action<TValue>? Pub;
public Trima(int period) public Trima(int period)
@@ -69,10 +70,10 @@ public sealed class Trima : ITValuePublisher
{ {
if (double.IsFinite(input)) if (double.IsFinite(input))
{ {
_lastValidValue1 = input; _state.LastValidValue1 = input;
return input; return input;
} }
return _lastValidValue1; return _state.LastValidValue1;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -80,61 +81,72 @@ public sealed class Trima : ITValuePublisher
{ {
if (isNew) if (isNew)
{ {
_sampleCount++; _p_state = _state;
_state.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);
} }
else else
{ {
// SMA 1 Correction _state = _p_state;
_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);
} }
// 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); Pub?.Invoke(Last);
return Last; return Last;
} }
@@ -203,14 +215,8 @@ public sealed class Trima : ITValuePublisher
{ {
_buffer1.Clear(); _buffer1.Clear();
_buffer2.Clear(); _buffer2.Clear();
_state = default;
_sum1 = _p_sum1 = _p_lastInput1 = _lastValidValue1 = _p_lastValidValue1 = 0; _p_state = default;
_tickCount1 = 0;
_sum2 = _p_sum2 = _p_lastInput2 = 0;
_tickCount2 = 0;
_sampleCount = 0;
Last = default; Last = default;
} }
} }
+34 -76
View File
@@ -31,12 +31,13 @@ public sealed class Vidya : ITValuePublisher
private readonly RingBuffer _ups; private readonly RingBuffer _ups;
private readonly RingBuffer _downs; private readonly RingBuffer _downs;
private double _prevClose; private record struct State(
private double _lastVidya; double PrevClose, double LastVidya,
private double _currentClose; double CurrentClose, double CurrentVidya,
private double _currentVidya; bool IsInitialized, int BarCount
private bool _isInitialized; );
private int _barCount; private State _state;
private State _p_state;
/// <summary> /// <summary>
/// Display name for the indicator. /// Display name for the indicator.
@@ -78,12 +79,18 @@ public sealed class Vidya : ITValuePublisher
{ {
if (isNew) if (isNew)
{ {
_barCount++; _p_state = _state;
if (_isInitialized) }
{ else
_prevClose = _currentClose; {
_lastVidya = _currentVidya; _state = _p_state;
} }
_state.BarCount++;
if (_state.IsInitialized)
{
_state.PrevClose = _state.CurrentClose;
_state.LastVidya = _state.CurrentVidya;
} }
double price = input.Value; double price = input.Value;
@@ -91,25 +98,25 @@ public sealed class Vidya : ITValuePublisher
{ {
// Handle NaN/Infinity by using the last known valid values // Handle NaN/Infinity by using the last known valid values
// If not initialized, we can't do much, just return input // If not initialized, we can't do much, just return input
if (!_isInitialized) return input; if (!_state.IsInitialized) return input;
price = _currentClose; // Use last valid close price = _state.CurrentClose; // Use last valid close
} }
if (_barCount <= 1) if (_state.BarCount <= 1)
{ {
_prevClose = price; _state.PrevClose = price;
_lastVidya = price; _state.LastVidya = price;
_currentClose = price; _state.CurrentClose = price;
_currentVidya = price; _state.CurrentVidya = price;
_isInitialized = true; _state.IsInitialized = true;
_ups.Add(0, isNew); _ups.Add(0, isNew);
_downs.Add(0, isNew); _downs.Add(0, isNew);
Last = new TValue(input.Time, _currentVidya); Last = new TValue(input.Time, _state.CurrentVidya);
Pub?.Invoke(Last); Pub?.Invoke(Last);
return Last; return Last;
} }
double change = price - _prevClose; double change = price - _state.PrevClose;
double up = change > 0 ? change : 0; double up = change > 0 ? change : 0;
double down = change < 0 ? -change : 0; double down = change < 0 ? -change : 0;
@@ -127,10 +134,10 @@ public sealed class Vidya : ITValuePublisher
} }
double dynamicAlpha = _alpha * vi; double dynamicAlpha = _alpha * vi;
_currentVidya = dynamicAlpha * price + (1.0 - dynamicAlpha) * _lastVidya; _state.CurrentVidya = dynamicAlpha * price + (1.0 - dynamicAlpha) * _state.LastVidya;
_currentClose = price; _state.CurrentClose = price;
Last = new TValue(input.Time, _currentVidya); Last = new TValue(input.Time, _state.CurrentVidya);
Pub?.Invoke(Last); Pub?.Invoke(Last);
return Last; return Last;
} }
@@ -150,42 +157,10 @@ public sealed class Vidya : ITValuePublisher
var sourceValues = source.Values; var sourceValues = source.Values;
var sourceTimes = source.Times; 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); Calculate(sourceValues, vSpan, _period);
sourceTimes.CopyTo(tSpan); 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(); Reset();
for (int i = 0; i < len; i++) for (int i = 0; i < len; i++)
{ {
@@ -210,14 +185,6 @@ public sealed class Vidya : ITValuePublisher
double alpha = 2.0 / (period + 1); 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[] ups = new double[period];
double[] downs = new double[period]; double[] downs = new double[period];
int head = 0; int head = 0;
@@ -227,11 +194,8 @@ public sealed class Vidya : ITValuePublisher
double prevClose = source[0]; double prevClose = source[0];
double lastVidya = source[0]; double lastVidya = source[0];
// Initialize first element
output[0] = source[0]; output[0] = source[0];
// Fill buffers with 0 initially (already done by new double[])
for (int i = 1; i < source.Length; i++) for (int i = 1; i < source.Length; i++)
{ {
double price = source[i]; double price = source[i];
@@ -244,7 +208,6 @@ public sealed class Vidya : ITValuePublisher
double up = change > 0 ? change : 0; double up = change > 0 ? change : 0;
double down = change < 0 ? -change : 0; double down = change < 0 ? -change : 0;
// Update sums: remove old, add new
sumUp -= ups[head]; sumUp -= ups[head];
sumDown -= downs[head]; sumDown -= downs[head];
@@ -277,13 +240,8 @@ public sealed class Vidya : ITValuePublisher
{ {
_ups.Clear(); _ups.Clear();
_downs.Clear(); _downs.Clear();
_prevClose = 0; _state = default;
_lastVidya = 0; _p_state = default;
_currentClose = 0;
_currentVidya = 0;
_isInitialized = false;
_barCount = 0;
Last = default; Last = default;
} }
} }
+32 -37
View File
@@ -32,10 +32,10 @@ public sealed class Wma : ITValuePublisher
private readonly double _divisor; private readonly double _divisor;
private readonly RingBuffer _buffer; private readonly RingBuffer _buffer;
private double _sum, _wsum; private record struct State(double Sum, double WSum, double LastInput, double LastValidValue, int TickCount);
private double _p_sum, _p_wsum, _p_lastInput; private State _state;
private double _lastValidValue, _p_lastValidValue; private State _p_state;
private int _tickCount;
private const int ResyncInterval = 1000; private const int ResyncInterval = 1000;
public string Name { get; } public string Name { get; }
@@ -63,10 +63,10 @@ public sealed class Wma : ITValuePublisher
{ {
if (double.IsFinite(input)) if (double.IsFinite(input))
{ {
_lastValidValue = input; _state.LastValidValue = input;
return input; return input;
} }
return _lastValidValue; return _state.LastValidValue;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -74,24 +74,24 @@ public sealed class Wma : ITValuePublisher
{ {
if (_buffer.IsFull) if (_buffer.IsFull)
{ {
double oldSum = _sum; double oldSum = _state.Sum;
double oldest = _buffer.Oldest; double oldest = _buffer.Oldest;
_sum = _sum - oldest + val; _state.Sum = _state.Sum - oldest + val;
_wsum = _wsum - oldSum + (_period * val); _state.WSum = _state.WSum - oldSum + (_period * val);
} }
else else
{ {
int count = _buffer.Count + 1; int count = _buffer.Count + 1;
_sum += val; _state.Sum += val;
_wsum += count * val; _state.WSum += count * val;
} }
_buffer.Add(val); _buffer.Add(val);
_tickCount++; _state.TickCount++;
if (_buffer.IsFull && _tickCount >= ResyncInterval) if (_buffer.IsFull && _state.TickCount >= ResyncInterval)
{ {
_tickCount = 0; _state.TickCount = 0;
double recalcSum = 0; double recalcSum = 0;
double recalcWsum = 0; double recalcWsum = 0;
int weight = 1; int weight = 1;
@@ -101,8 +101,8 @@ public sealed class Wma : ITValuePublisher
recalcWsum += weight * item; recalcWsum += weight * item;
weight++; weight++;
} }
_sum = recalcSum; _state.Sum = recalcSum;
_wsum = recalcWsum; _state.WSum = recalcWsum;
} }
} }
@@ -113,29 +113,24 @@ public sealed class Wma : ITValuePublisher
{ {
double val = GetValidValue(input.Value); double val = GetValidValue(input.Value);
UpdateState(val); UpdateState(val);
_state.LastInput = val;
_p_sum = _sum; _p_state = _state;
_p_wsum = _wsum;
_p_lastInput = val;
_p_lastValidValue = _lastValidValue;
} }
else else
{ {
_lastValidValue = _p_lastValidValue; _state = _p_state;
double val = GetValidValue(input.Value); double val = GetValidValue(input.Value);
_sum = _p_sum;
_wsum = _p_wsum;
int weight = _buffer.IsFull ? _period : _buffer.Count; int weight = _buffer.IsFull ? _period : _buffer.Count;
_sum = _sum - _p_lastInput + val; _state.Sum = _state.Sum - _state.LastInput + val;
_wsum += weight * (val - _p_lastInput); _state.WSum += weight * (val - _state.LastInput);
_buffer.UpdateNewest(val); _buffer.UpdateNewest(val);
} }
double currentDivisor = _buffer.IsFull ? _divisor : (double)_buffer.Count * (_buffer.Count + 1) * 0.5; 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); Pub?.Invoke(Last);
return Last; return Last;
} }
@@ -162,35 +157,34 @@ public sealed class Wma : ITValuePublisher
if (startIndex > 0) if (startIndex > 0)
{ {
_state.LastValidValue = 0;
for (int i = startIndex - 1; i >= 0; i--) for (int i = startIndex - 1; i >= 0; i--)
{ {
if (double.IsFinite(source.Values[i])) if (double.IsFinite(source.Values[i]))
{ {
_lastValidValue = source.Values[i]; _state.LastValidValue = source.Values[i];
break; break;
} }
} }
} }
else else
{ {
_lastValidValue = 0; _state.LastValidValue = 0;
} }
_buffer.Clear(); _buffer.Clear();
_sum = 0; _state.Sum = 0;
_wsum = 0; _state.WSum = 0;
_tickCount = 0; _state.TickCount = 0;
for (int i = startIndex; i < len; i++) for (int i = startIndex; i < len; i++)
{ {
double val = GetValidValue(source.Values[i]); double val = GetValidValue(source.Values[i]);
UpdateState(val); UpdateState(val);
_state.LastInput = val;
} }
_p_sum = _sum; _p_state = _state;
_p_wsum = _wsum;
_p_lastInput = GetValidValue(source.Values[len - 1]);
_p_lastValidValue = _lastValidValue;
Last = new TValue(tSpan[len - 1], vSpan[len - 1]); Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
return new TSeries(t, v); return new TSeries(t, v);
@@ -754,7 +748,8 @@ public sealed class Wma : ITValuePublisher
public void Reset() public void Reset()
{ {
_buffer.Clear(); _buffer.Clear();
_sum = _wsum = _p_sum = _p_wsum = _p_lastInput = _lastValidValue = _p_lastValidValue = 0; _state = default;
_p_state = default;
Last = default; Last = default;
} }
} }
+23
View File
@@ -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}");
}
}