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
+29 -11
View File
@@ -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);
}
/// <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>
/// Generates next standard normal using Box-Muller transform with caching.
/// </summary>
@@ -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;
+18 -5
View File
@@ -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;
/// <summary>
/// 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;
}
}
+14 -15
View File
@@ -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;
}
}
+1 -20
View File
@@ -24,28 +24,9 @@ namespace QuanTAlib;
[SkipLocalsInit]
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 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;
+1 -20
View File
@@ -27,28 +27,9 @@ namespace QuanTAlib;
[SkipLocalsInit]
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 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;
+52 -37
View File
@@ -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;
/// <summary>
/// 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;
}
}
+29 -43
View File
@@ -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;
}
}
+39 -70
View File
@@ -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<TValue>? 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;
}
+24 -34
View File
@@ -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;
}
}
+1 -19
View File
@@ -26,27 +26,9 @@ namespace QuanTAlib;
[SkipLocalsInit]
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 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>
+1 -20
View File
@@ -26,28 +26,9 @@ namespace QuanTAlib;
[SkipLocalsInit]
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 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;
+73 -67
View File
@@ -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<TValue>? 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;
}
}
+34 -76
View File
@@ -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;
/// <summary>
/// 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;
}
}
+32 -37
View File
@@ -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;
}
}