style patterns

This commit is contained in:
Miha Kralj
2026-01-25 16:01:45 -08:00
parent 2836f253c4
commit e59665c8f0
399 changed files with 6892 additions and 1323 deletions
+39 -6
View File
@@ -49,7 +49,10 @@ public sealed class Dema : AbstractBase
public Dema(int period)
{
if (period <= 0) throw new ArgumentException("Period must be greater than 0", nameof(period));
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
_alpha = 2.0 / (period + 1);
_decay = 1.0 - _alpha;
@@ -66,7 +69,10 @@ public sealed class Dema : AbstractBase
public Dema(double alpha)
{
if (alpha <= 0 || alpha > 1) throw new ArgumentException("Alpha must be between 0 and 1", nameof(alpha));
if (alpha <= 0 || alpha > 1)
{
throw new ArgumentException("Alpha must be between 0 and 1", nameof(alpha));
}
_alpha = alpha;
_decay = 1.0 - alpha;
@@ -94,9 +100,13 @@ public sealed class Dema : AbstractBase
// EMA1
double val = input.Value;
if (double.IsFinite(val))
{
_lastValidValue = val;
}
else
{
val = _lastValidValue;
}
if (double.IsNaN(val))
{
@@ -118,7 +128,10 @@ public sealed class Dema : AbstractBase
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
int len = source.Count;
List<long> t = new(len);
@@ -148,9 +161,13 @@ public sealed class Dema : AbstractBase
{
double val = sourceValues[i];
if (double.IsFinite(val))
{
lastValid = val;
}
else
{
val = lastValid;
}
if (double.IsNaN(val))
{
@@ -197,7 +214,9 @@ public sealed class Dema : AbstractBase
state.E *= decay;
if (!state.IsHot && state.E <= 0.05) // COVERAGE_THRESHOLD
{
state.IsHot = true;
}
if (state.E <= 1e-10) // COMPENSATOR_THRESHOLD
{
@@ -232,7 +251,9 @@ public sealed class Dema : AbstractBase
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
double alpha = 2.0 / (period + 1);
Calculate(source, output, alpha);
@@ -241,11 +262,19 @@ public sealed class Dema : AbstractBase
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, double alpha)
{
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output must have the same length", nameof(output));
if (alpha <= 0 || alpha > 1)
throw new ArgumentException("Alpha must be between 0 and 1", nameof(alpha));
}
if (source.Length == 0) return;
if (alpha <= 0 || alpha > 1)
{
throw new ArgumentException("Alpha must be between 0 and 1", nameof(alpha));
}
if (source.Length == 0)
{
return;
}
double decay = 1.0 - alpha;
double lastValid = double.NaN;
@@ -264,9 +293,13 @@ public sealed class Dema : AbstractBase
{
double val = source[i];
if (double.IsFinite(val))
{
lastValid = val;
}
else
{
val = lastValid;
}
if (double.IsNaN(val))
{
+1 -1
View File
@@ -199,7 +199,7 @@ public class DsmaIndicatorTests
// Process first bar
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
// Process second bar as new
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
double afterNewBar = indicator.LinesSeries[0].GetValue(0);
+2 -2
View File
@@ -232,7 +232,7 @@ public class DsmaTests
// Get the last bar again after loop
lastBar = gbm.Next(isNew: false);
// Inject NaN
var nanResult = dsma.Update(new TValue(lastBar.Time, double.NaN));
@@ -257,7 +257,7 @@ public class DsmaTests
// Get the last bar again after loop
lastBar = gbm.Next(isNew: false);
// Inject Infinity
var infResult = dsma.Update(new TValue(lastBar.Time, double.PositiveInfinity));
var negInfResult = dsma.Update(new TValue(lastBar.Time, double.NegativeInfinity));
+14 -7
View File
@@ -58,7 +58,7 @@ public class DsmaValidationTests
}
// In higher volatility, absolute deviation should generally be larger
Assert.True(highVolDeviation > lowVolDeviation * 2,
Assert.True(highVolDeviation > lowVolDeviation * 2,
$"High volatility deviation {highVolDeviation:F2} should be significantly larger than low volatility {lowVolDeviation:F2}");
}
@@ -96,7 +96,7 @@ public class DsmaValidationTests
double avgHighLag = highScaleLag / count;
// Lower scale factor should have higher average lag (smoother, less responsive)
Assert.True(avgLowLag > avgHighLag,
Assert.True(avgLowLag > avgHighLag,
$"Low scale lag {avgLowLag:F4} should be greater than high scale lag {avgHighLag:F4}");
}
@@ -133,7 +133,7 @@ public class DsmaValidationTests
double dsmaVariance = dsmaChanges.Average();
// DSMA should have lower variance than raw price
Assert.True(dsmaVariance < priceVariance,
Assert.True(dsmaVariance < priceVariance,
$"DSMA variance {dsmaVariance:F4} should be less than price variance {priceVariance:F4}");
}
@@ -199,7 +199,7 @@ public class DsmaValidationTests
var gbm = new GBM(startPrice: 100.0, mu: 0.01, sigma: 0.2, seed: 654);
var series = new TSeries();
for (int i = 0; i < 100; i++)
{
var bar = gbm.Next(isNew: true);
@@ -254,7 +254,7 @@ public class DsmaValidationTests
double avgLongLag = longLag / count;
// Longer period should have higher average lag (more smoothing)
Assert.True(avgLongLag > avgShortLag,
Assert.True(avgLongLag > avgShortLag,
$"Long period lag {avgLongLag:F4} should be greater than short period lag {avgShortLag:F4}");
}
@@ -319,8 +319,15 @@ public class DsmaValidationTests
for (int i = 1; i < prices.Count; i++)
{
if (prices[i] > prices[i - 1]) priceUpCount++;
if (dsmaValues[i] > dsmaValues[i - 1]) dsmaUpCount++;
if (prices[i] > prices[i - 1])
{
priceUpCount++;
}
if (dsmaValues[i] > dsmaValues[i - 1])
{
dsmaUpCount++;
}
}
// DSMA should have similar directional trend but smoother
+15 -1
View File
@@ -89,9 +89,14 @@ public sealed class Dsma : AbstractBase
public Dsma(int period, double scaleFactor = 0.5)
{
if (period < 2)
{
throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 2.");
}
if (scaleFactor < 0.01 || scaleFactor > 0.9)
{
throw new ArgumentOutOfRangeException(nameof(scaleFactor), "Scale factor must be between 0.01 and 0.9.");
}
WarmupPeriod = period;
_periodRecip = 1.0 / period;
@@ -146,12 +151,16 @@ public sealed class Dsma : AbstractBase
HandleStateSnapshot(isNew);
value = HandleInvalidInput(value);
if (double.IsNaN(value))
{
return double.NaN;
}
_state.Bars++;
if (_state.Bars == 1)
{
return InitializeFirstBar(value);
}
return CalculateDsma(value);
}
@@ -256,7 +265,10 @@ public sealed class Dsma : AbstractBase
/// <returns>Time series containing DSMA values</returns>
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
int len = source.Count;
var t = new List<long>(len);
@@ -325,7 +337,9 @@ public sealed class Dsma : AbstractBase
double scaleFactor = 0.5)
{
if (output.Length < source.Length)
{
throw new ArgumentException("Output span is shorter than source span.", nameof(output));
}
var dsma = new Dsma(period, scaleFactor);
for (int i = 0; i < source.Length; i++)
+14 -3
View File
@@ -444,7 +444,9 @@ public class EmaTests
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
for (int i = 0; i < source.Length; i++)
{
source[i] = gbm.Next().Close;
}
// Warm up
Ema.Batch(source.AsSpan(), output.AsSpan(), 100);
@@ -525,7 +527,10 @@ public class EmaTests
// ...
// We can verify against a fresh EMA fed with same data
var verifyEma = new Ema(5);
foreach (var val in history) verifyEma.Update(new TValue(DateTime.UtcNow, val));
foreach (var val in history)
{
verifyEma.Update(new TValue(DateTime.UtcNow, val));
}
Assert.Equal(verifyEma.Last.Value, ema.Last.Value, 1e-10);
Assert.Equal(verifyEma.IsHot, ema.IsHot);
@@ -545,7 +550,10 @@ public class EmaTests
ema.Prime(history);
var verifyEma = new Ema(5);
foreach (var val in history) verifyEma.Update(new TValue(DateTime.UtcNow, val));
foreach (var val in history)
{
verifyEma.Update(new TValue(DateTime.UtcNow, val));
}
Assert.Equal(verifyEma.Last.Value, ema.Last.Value, 1e-10);
}
@@ -565,7 +573,10 @@ public class EmaTests
public void Calculate_ReturnsCorrectResultsAndHotIndicator()
{
var series = new TSeries();
for (int i = 1; i <= 20; i++) series.Add(DateTime.UtcNow, i * 10);
for (int i = 1; i <= 20; i++)
{
series.Add(DateTime.UtcNow, i * 10);
}
// EMA(5)
var (results, indicator) = Ema.Calculate(series, 5);
+76 -9
View File
@@ -91,7 +91,9 @@ public sealed class Ema : AbstractBase
public Ema(double alpha)
{
if (alpha <= 0 || alpha > 1)
{
throw new ArgumentException("Alpha must be greater than 0 and at most 1", nameof(alpha));
}
_alpha = alpha;
_decay = 1.0 - alpha;
@@ -118,7 +120,10 @@ public sealed class Ema : AbstractBase
/// <param name="source">Historical data</param>
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
if (source.Length == 0) return;
if (source.Length == 0)
{
return;
}
// Reset state
_state = State.New();
@@ -170,7 +175,9 @@ public sealed class Ema : AbstractBase
finally
{
if (rented != null)
{
ArrayPool<double>.Shared.Return(rented);
}
}
}
@@ -215,7 +222,10 @@ public sealed class Ema : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
int len = source.Count;
var t = new List<long>(len);
@@ -262,7 +272,9 @@ public sealed class Ema : AbstractBase
state.E *= decay;
if (!state.IsHot && state.E <= COVERAGE_THRESHOLD)
{
state.IsHot = true;
}
if (state.E <= COMPENSATOR_THRESHOLD)
{
@@ -300,21 +312,29 @@ public sealed class Ema : AbstractBase
{
double val = source[i];
if (double.IsFinite(val))
{
lastValidValue = val;
}
else
{
val = lastValidValue;
}
state.Ema = Math.FusedMultiplyAdd(state.Ema, decay, alpha * val);
state.E *= decay;
if (!state.IsHot && state.E <= COVERAGE_THRESHOLD)
{
state.IsHot = true;
}
output[i] = state.Ema / (1.0 - state.E);
state.TickCount++;
}
if (state.E <= COMPENSATOR_THRESHOLD)
{
state.IsCompensated = true;
}
}
// Phase 2: Post-compensation (hot path) - optimized with loop unrolling
@@ -328,22 +348,54 @@ public sealed class Ema : AbstractBase
for (; i < unrollEnd; i += 4)
{
double v0 = Unsafe.Add(ref srcRef, i);
if (!double.IsFinite(v0)) v0 = lastValidValue; else lastValidValue = v0;
if (!double.IsFinite(v0))
{
v0 = lastValidValue;
}
else
{
lastValidValue = v0;
}
state.Ema = Math.FusedMultiplyAdd(state.Ema, decay, alpha * v0);
Unsafe.Add(ref outRef, i) = state.Ema;
double v1 = Unsafe.Add(ref srcRef, i + 1);
if (!double.IsFinite(v1)) v1 = lastValidValue; else lastValidValue = v1;
if (!double.IsFinite(v1))
{
v1 = lastValidValue;
}
else
{
lastValidValue = v1;
}
state.Ema = Math.FusedMultiplyAdd(state.Ema, decay, alpha * v1);
Unsafe.Add(ref outRef, i + 1) = state.Ema;
double v2 = Unsafe.Add(ref srcRef, i + 2);
if (!double.IsFinite(v2)) v2 = lastValidValue; else lastValidValue = v2;
if (!double.IsFinite(v2))
{
v2 = lastValidValue;
}
else
{
lastValidValue = v2;
}
state.Ema = Math.FusedMultiplyAdd(state.Ema, decay, alpha * v2);
Unsafe.Add(ref outRef, i + 2) = state.Ema;
double v3 = Unsafe.Add(ref srcRef, i + 3);
if (!double.IsFinite(v3)) v3 = lastValidValue; else lastValidValue = v3;
if (!double.IsFinite(v3))
{
v3 = lastValidValue;
}
else
{
lastValidValue = v3;
}
state.Ema = Math.FusedMultiplyAdd(state.Ema, decay, alpha * v3);
Unsafe.Add(ref outRef, i + 3) = state.Ema;
@@ -364,7 +416,14 @@ public sealed class Ema : AbstractBase
for (; i < len; i++)
{
double val = Unsafe.Add(ref srcRef, i);
if (!double.IsFinite(val)) val = lastValidValue; else lastValidValue = val;
if (!double.IsFinite(val))
{
val = lastValidValue;
}
else
{
lastValidValue = val;
}
state.Ema = Math.FusedMultiplyAdd(state.Ema, decay, alpha * val);
Unsafe.Add(ref outRef, i) = state.Ema;
@@ -469,7 +528,9 @@ public sealed class Ema : AbstractBase
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
double alpha = 2.0 / (period + 1);
Batch(source, output, alpha);
@@ -486,11 +547,17 @@ public sealed class Ema : AbstractBase
public static void Batch(ReadOnlySpan<double> source, Span<double> output, double alpha)
{
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output must have the same length", nameof(output));
}
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(alpha, 0.0);
ArgumentOutOfRangeException.ThrowIfGreaterThan(alpha, 1.0);
if (source.Length == 0) return;
if (source.Length == 0)
{
return;
}
// For large, clean datasets, use optimized path without NaN handling
if (source.Length >= CleanPathThreshold && !source.ContainsNonFinite())
@@ -535,4 +602,4 @@ public sealed class Ema : AbstractBase
_p_lastValidValue = 0;
Last = default;
}
}
}
+36 -8
View File
@@ -110,18 +110,39 @@ public class FramaValidationTests
lv = lastLow;
}
if (hv > maxFull) maxFull = hv;
if (lv < minFull) minFull = lv;
if (hv > maxFull)
{
maxFull = hv;
}
if (lv < minFull)
{
minFull = lv;
}
if (j >= startRecent)
{
if (hv > maxRecent) maxRecent = hv;
if (lv < minRecent) minRecent = lv;
if (hv > maxRecent)
{
maxRecent = hv;
}
if (lv < minRecent)
{
minRecent = lv;
}
}
else
{
if (hv > maxPrev) maxPrev = hv;
if (lv < minPrev) minPrev = lv;
if (hv > maxPrev)
{
maxPrev = hv;
}
if (lv < minPrev)
{
minPrev = lv;
}
}
}
@@ -134,8 +155,15 @@ public class FramaValidationTests
{
double dimen = (Math.Log(n1 + n2) - Math.Log(n3)) / 0.693147180559945309417232121458176568;
alpha = Math.Exp(-4.6 * (dimen - 1.0));
if (alpha < 0.01) alpha = 0.01;
if (alpha > 1.0) alpha = 1.0;
if (alpha < 0.01)
{
alpha = 0.01;
}
if (alpha > 1.0)
{
alpha = 1.0;
}
}
double price = (highVal + lowVal) * 0.5;
+40 -6
View File
@@ -147,8 +147,15 @@ public sealed class Frama : ITValuePublisher
{
double dimen = (Math.Log(n1 + n2) - Math.Log(n3)) / Log2;
alpha = Math.Exp(-4.6 * (dimen - 1.0));
if (alpha < AlphaFloor) alpha = AlphaFloor;
if (alpha > AlphaCeil) alpha = AlphaCeil;
if (alpha < AlphaFloor)
{
alpha = AlphaFloor;
}
if (alpha > AlphaCeil)
{
alpha = AlphaCeil;
}
}
double prev = _state.HasValue && double.IsFinite(_state.Frama) ? _state.Frama : price;
@@ -170,7 +177,10 @@ public sealed class Frama : ITValuePublisher
public TSeries Update(TBarSeries source)
{
if (source.Count == 0) return new TSeries([], []);
if (source.Count == 0)
{
return new TSeries([], []);
}
int len = source.Count;
var v = new double[len];
@@ -195,7 +205,10 @@ public sealed class Frama : ITValuePublisher
public TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
int len = source.Count;
var t = new List<long>(len);
@@ -224,7 +237,9 @@ public sealed class Frama : ITValuePublisher
public static void Calculate(ReadOnlySpan<double> high, ReadOnlySpan<double> low, int period, Span<double> output)
{
if (high.Length != low.Length || high.Length != output.Length)
{
throw new ArgumentException("Input spans must have the same length.", nameof(output));
}
ArgumentOutOfRangeException.ThrowIfLessThan(period, 2);
@@ -240,7 +255,9 @@ public sealed class Frama : ITValuePublisher
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period)
{
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output must have the same length.", nameof(output));
}
ArgumentOutOfRangeException.ThrowIfLessThan(period, 2);
@@ -254,7 +271,10 @@ public sealed class Frama : ITValuePublisher
public static TSeries Batch(TBarSeries source, int period)
{
if (source.Count == 0) return new TSeries([], []);
if (source.Count == 0)
{
return new TSeries([], []);
}
int len = source.Count;
var v = new double[len];
@@ -275,7 +295,9 @@ public sealed class Frama : ITValuePublisher
{
int count = buffer.Count;
if (count == 0 || length <= 0)
{
return double.NaN;
}
int capacity = buffer.Capacity;
int start = buffer.StartIndex;
@@ -288,10 +310,15 @@ public sealed class Frama : ITValuePublisher
{
int idx = start + offset + i;
if (idx >= capacity)
{
idx -= capacity;
}
double v = data[idx];
if (v > max)
{
max = v;
}
}
return max;
@@ -302,7 +329,9 @@ public sealed class Frama : ITValuePublisher
{
int count = buffer.Count;
if (count == 0 || length <= 0)
{
return double.NaN;
}
int capacity = buffer.Capacity;
int start = buffer.StartIndex;
@@ -315,12 +344,17 @@ public sealed class Frama : ITValuePublisher
{
int idx = start + offset + i;
if (idx >= capacity)
{
idx -= capacity;
}
double v = data[idx];
if (v < min)
{
min = v;
}
}
return min;
}
}
}
@@ -90,9 +90,13 @@ public class HemaValidationTests
{
double val = source[i];
if (double.IsFinite(val))
{
lastValid = val;
}
else
{
val = lastValid;
}
if (double.IsNaN(val))
{
+44 -3
View File
@@ -119,9 +119,13 @@ public sealed class Hema : AbstractBase
double val = input.Value;
if (double.IsFinite(val))
{
_lastValidValue = val;
}
else
{
val = _lastValidValue;
}
if (double.IsNaN(val))
{
@@ -139,7 +143,10 @@ public sealed class Hema : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
int len = source.Count;
List<long> t = new(len);
@@ -163,9 +170,13 @@ public sealed class Hema : AbstractBase
{
double val = sourceValues[i];
if (double.IsFinite(val))
{
lastValid = val;
}
else
{
val = lastValid;
}
if (double.IsNaN(val))
{
@@ -214,17 +225,23 @@ public sealed class Hema : AbstractBase
double emaFast = state.EmaFastRaw * invFast;
double deLag = Math.FusedMultiplyAdd(-_ratio, emaSlow, emaFast) * _invOneMinusRatio;
if (!double.IsFinite(deLag))
{
deLag = input;
}
state.EmaSmoothRaw = Math.FusedMultiplyAdd(state.EmaSmoothRaw, _betaSmooth, _alphaSmooth * deLag);
double maxDecay = Math.Max(state.DecaySlow, Math.Max(state.DecayFast, state.DecaySmooth));
if (!state.IsHot && maxDecay <= CoverageThreshold)
{
state.IsHot = true;
}
state.Warmup = maxDecay > CompensatorThreshold;
if (!state.Warmup)
{
state.IsHot = true;
}
double result = state.EmaSmoothRaw * invSmooth;
if (!double.IsFinite(result))
@@ -238,11 +255,16 @@ public sealed class Hema : AbstractBase
double deLagFast = Math.FusedMultiplyAdd(-_ratio, state.EmaSlowRaw, state.EmaFastRaw) * _invOneMinusRatio;
if (!double.IsFinite(deLagFast))
{
deLagFast = input;
}
state.EmaSmoothRaw = Math.FusedMultiplyAdd(state.EmaSmoothRaw, _betaSmooth, _alphaSmooth * deLagFast);
if (!state.IsHot)
{
state.IsHot = true;
}
double fastResult = state.EmaSmoothRaw;
if (!double.IsFinite(fastResult))
@@ -263,10 +285,16 @@ public sealed class Hema : AbstractBase
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period)
{
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output must have the same length", nameof(output));
}
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(period);
if (source.Length == 0) return;
if (source.Length == 0)
{
return;
}
double n = Math.Max((double)period, 2.0);
double alphaSlow = AlphaFromHalfLife(n);
@@ -296,9 +324,13 @@ public sealed class Hema : AbstractBase
{
double val = source[i];
if (double.IsFinite(val))
{
lastValid = val;
}
else
{
val = lastValid;
}
if (double.IsNaN(val))
{
@@ -323,7 +355,9 @@ public sealed class Hema : AbstractBase
double emaFast = emaFastRaw * invFast;
double deLag = Math.FusedMultiplyAdd(-ratio, emaSlow, emaFast) * invOneMinusRatio;
if (!double.IsFinite(deLag))
{
deLag = val;
}
emaSmoothRaw = Math.FusedMultiplyAdd(emaSmoothRaw, betaSmooth, alphaSmooth * deLag);
double result = emaSmoothRaw * invSmooth;
@@ -348,7 +382,10 @@ public sealed class Hema : AbstractBase
{
double deLag = Math.FusedMultiplyAdd(-ratio, emaSlowRaw, emaFastRaw) * invOneMinusRatio;
if (!double.IsFinite(deLag))
{
deLag = val;
}
emaSmoothRaw = Math.FusedMultiplyAdd(emaSmoothRaw, betaSmooth, alphaSmooth * deLag);
double result = emaSmoothRaw;
if (!double.IsFinite(result))
@@ -424,12 +461,16 @@ public sealed class Hema : AbstractBase
{
double maxDecay = Math.Max(_betaSlow, Math.Max(_betaFast, _betaSmooth));
if (maxDecay <= 0)
{
return 1;
}
double steps = Math.Log(CoverageThreshold) / Math.Log(maxDecay);
if (double.IsNaN(steps) || double.IsInfinity(steps) || steps <= 0)
{
return 1;
}
return (int)Math.Ceiling(steps);
}
}
}
+47 -11
View File
@@ -195,11 +195,25 @@ public sealed class Htit : AbstractBase
{
double cap = 1.5 * prevPeriod;
double floor = 0.67 * prevPeriod;
if (period > cap) period = cap;
if (period < floor) period = floor;
if (period > cap)
{
period = cap;
}
if (period < floor)
{
period = floor;
}
}
if (period < 6)
{
period = 6;
}
if (period > 50)
{
period = 50;
}
if (period < 6) period = 6;
if (period > 50) period = 50;
// Smooth the period (using FMA)
_state.Period = Math.FusedMultiplyAdd(0.2, period, 0.8 * prevPeriod);
@@ -254,7 +268,10 @@ public sealed class Htit : AbstractBase
/// <returns>Output time series with HTIT values</returns>
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return new TSeries([], []);
if (source.Count == 0)
{
return new TSeries([], []);
}
int len = source.Count;
var v = new List<double>(len);
@@ -293,9 +310,14 @@ public sealed class Htit : AbstractBase
public static void Calculate(ReadOnlySpan<double> source, Span<double> output)
{
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output must have the same length", nameof(output));
}
if (source.Length == 0) return;
if (source.Length == 0)
{
return;
}
// Stack allocate buffers
// priceBuffer needs to be larger for IT calculation (up to 50 bars)
@@ -416,11 +438,25 @@ public sealed class Htit : AbstractBase
{
double cap = 1.5 * p_period;
double floor = 0.67 * p_period;
if (newPeriod > cap) newPeriod = cap;
if (newPeriod < floor) newPeriod = floor;
if (newPeriod > cap)
{
newPeriod = cap;
}
if (newPeriod < floor)
{
newPeriod = floor;
}
}
if (newPeriod < 6)
{
newPeriod = 6;
}
if (newPeriod > 50)
{
newPeriod = 50;
}
if (newPeriod < 6) newPeriod = 6;
if (newPeriod > 50) newPeriod = 50;
period = Math.FusedMultiplyAdd(0.2, newPeriod, 0.8 * p_period);
smoothPeriod = Math.FusedMultiplyAdd(0.33, period, 0.67 * p_smoothPeriod);
@@ -475,4 +511,4 @@ public sealed class Htit : AbstractBase
}
}
}
}
}
+1 -1
View File
@@ -310,4 +310,4 @@ public class JmaTests
Assert.True(double.IsFinite(restoredResult.Value));
Assert.True(jma.IsHot);
}
}
}
+1 -1
View File
@@ -39,4 +39,4 @@ public class JmaZeroDivTests
// With clamping, adapt is slightly non-zero (approx 1e-12), so result is very close to 200.
Assert.Equal(200, result.Value, precision: 7);
}
}
}
+45 -6
View File
@@ -63,17 +63,28 @@ public sealed class Jma : AbstractBase
public Jma(int period, int phase = 0, double power = 0.45)
{
if (period < 1)
{
throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 1.");
}
if (!double.IsFinite(power))
{
throw new ArgumentException("Power must be finite.", nameof(power));
}
// --- Phase parameter: maps -100..100 -> 0.5..2.5 (Jurik convention) ---
if (phase < -100)
{
_phaseParam = 0.5;
}
else if (phase > 100)
{
_phaseParam = 2.5;
}
else
{
_phaseParam = (phase * 0.01) + 1.5;
}
// --- Length / log / divider parameters (from decompiled JMA) ---
// L_raw ~ (period - 1)/2, with a tiny lower bound to avoid log(0)
@@ -136,7 +147,10 @@ public sealed class Jma : AbstractBase
if (!double.IsFinite(value))
{
if (_state.Bars == 0)
{
return double.NaN;
}
value = _state.LastPrice;
}
else
@@ -146,7 +160,9 @@ public sealed class Jma : AbstractBase
_state.Bars++;
if (_state.Bars == 1)
{
return InitializeFirstBar(value);
}
return CalculateJma(value);
}
@@ -215,8 +231,16 @@ public sealed class Jma : AbstractBase
{
double ratio = Math.Max(absValue / refVolatility, 0.0);
double d = Math.Pow(ratio, _pExponent);
if (d > _logParam) d = _logParam;
if (d < 1.0) d = 1.0;
if (d > _logParam)
{
d = _logParam;
}
if (d < 1.0)
{
d = 1.0;
}
return d;
}
@@ -271,7 +295,10 @@ public sealed class Jma : AbstractBase
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
int len = source.Count;
var t = new List<long>(len);
@@ -339,9 +366,14 @@ public sealed class Jma : AbstractBase
double power = 0.45)
{
if (output.Length != source.Length)
{
throw new ArgumentException("Source and output must have the same length.", nameof(output));
}
if (source.Length == 0)
{
return;
}
var jma = new Jma(period, phase, power);
for (int i = 0; i < source.Length; i++)
@@ -383,10 +415,17 @@ public sealed class Jma : AbstractBase
end = drop + slice - 1;
}
if (start < 0) start = 0;
if (end >= count) end = count - 1;
if (start < 0)
{
start = 0;
}
if (end >= count)
{
end = count - 1;
}
int len = end - start + 1;
return sorted.Slice(start, len).SumSIMD() / len;
}
}
}
+55 -9
View File
@@ -41,13 +41,24 @@ public sealed class Kama : AbstractBase
public Kama(int period = 10, int fastPeriod = 2, int slowPeriod = 30)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
if (fastPeriod <= 0)
{
throw new ArgumentException("Fast period must be greater than 0", nameof(fastPeriod));
}
if (slowPeriod <= 0)
{
throw new ArgumentException("Slow period must be greater than 0", nameof(slowPeriod));
}
if (fastPeriod >= slowPeriod)
{
throw new ArgumentException("Fast period must be less than slow period", nameof(fastPeriod));
}
// Buffer needs to hold period + 1 values to calculate Change over 'period' bars
// Change = Price[0] - Price[period]
@@ -161,7 +172,10 @@ public sealed class Kama : AbstractBase
// Avoid division by zero
double er = (volatility > 1e-10) ? change / volatility : 0.0;
// Cap ER at 1.0 just in case floating point errors push it slightly over
if (er > 1.0) er = 1.0;
if (er > 1.0)
{
er = 1.0;
}
// double sc = er * (_fastAlpha - _slowAlpha) + _slowAlpha; // skipcq: S125
double sc = Math.FusedMultiplyAdd(er, _fastAlpha - _slowAlpha, _slowAlpha);
@@ -184,7 +198,10 @@ public sealed class Kama : AbstractBase
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return new TSeries([], []);
if (source.Count == 0)
{
return new TSeries([], []);
}
int len = source.Count;
var t = new List<long>(len);
@@ -227,11 +244,30 @@ public sealed class Kama : AbstractBase
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period, int fastPeriod = 2, int slowPeriod = 30)
{
if (period <= 0) throw new ArgumentException("Period must be greater than 0", nameof(period));
if (fastPeriod <= 0) throw new ArgumentException("Fast period must be greater than 0", nameof(fastPeriod));
if (slowPeriod <= 0) throw new ArgumentException("Slow period must be greater than 0", nameof(slowPeriod));
if (fastPeriod >= slowPeriod) throw new ArgumentException("Fast period must be less than slow period", nameof(fastPeriod));
if (source.Length != output.Length) throw new ArgumentException("Source and output must have the same length", nameof(output));
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
if (fastPeriod <= 0)
{
throw new ArgumentException("Fast period must be greater than 0", nameof(fastPeriod));
}
if (slowPeriod <= 0)
{
throw new ArgumentException("Slow period must be greater than 0", nameof(slowPeriod));
}
if (fastPeriod >= slowPeriod)
{
throw new ArgumentException("Fast period must be less than slow period", nameof(fastPeriod));
}
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output must have the same length", nameof(output));
}
double fastAlpha = 2.0 / (fastPeriod + 1);
double slowAlpha = 2.0 / (slowPeriod + 1);
@@ -252,9 +288,13 @@ public sealed class Kama : AbstractBase
{
double val = source[i];
if (double.IsFinite(val))
{
lastValid = val;
}
else
{
val = lastValid;
}
if (double.IsNaN(val))
{
@@ -287,7 +327,10 @@ public sealed class Kama : AbstractBase
}
bufferIdx = (bufferIdx + 1) % bufSize;
if (count < bufSize) count++;
if (count < bufSize)
{
count++;
}
if (!kamaInitialized)
{
@@ -307,7 +350,10 @@ public sealed class Kama : AbstractBase
double change = Math.Abs(val - buffer[count == bufSize ? bufferIdx : 0]);
double er = (volatilitySum > 1e-10) ? change / volatilitySum : 0.0;
if (er > 1.0) er = 1.0;
if (er > 1.0)
{
er = 1.0;
}
// double sc = er * (fastAlpha - slowAlpha) + slowAlpha; // skipcq: S125
double sc = Math.FusedMultiplyAdd(er, fastAlpha - slowAlpha, slowAlpha);
+33 -8
View File
@@ -137,8 +137,15 @@ public class MamaTests
// Case 2: Update in chunks
var chunk1 = new TSeries();
var chunk2 = new TSeries();
for (int i = 0; i < 25; i++) chunk1.Add(data[i]);
for (int i = 25; i < 50; i++) chunk2.Add(data[i]);
for (int i = 0; i < 25; i++)
{
chunk1.Add(data[i]);
}
for (int i = 25; i < 50; i++)
{
chunk2.Add(data[i]);
}
mama2.Update(chunk1);
var result2 = mama2.Update(chunk2);
@@ -239,7 +246,10 @@ public class MamaTests
const int count = 100;
var data = new double[count];
var gbm = new GBM(startPrice: 100, seed: 42);
for (int i = 0; i < count; i++) data[i] = gbm.Next().Close;
for (int i = 0; i < count; i++)
{
data[i] = gbm.Next().Close;
}
var output = new double[count];
Mama.Calculate(data, output);
@@ -309,7 +319,10 @@ public class MamaTests
{
var data = new double[60];
var gbm = new GBM(startPrice: 100, seed: 42);
for (int i = 0; i < 60; i++) data[i] = gbm.Next().Close;
for (int i = 0; i < 60; i++)
{
data[i] = gbm.Next().Close;
}
// 1. Prime with all but last value
var mamaPrimed = new Mama();
@@ -336,7 +349,10 @@ public class MamaTests
int count = 100;
var data = new double[count];
var gbm = new GBM(startPrice: 100, seed: 42);
for (int i = 0; i < count; i++) data[i] = gbm.Next().Close;
for (int i = 0; i < count; i++)
{
data[i] = gbm.Next().Close;
}
var mamaOutput = new double[count];
var famaOutput = new double[count];
@@ -357,7 +373,10 @@ public class MamaTests
int count = 100;
var data = new double[count];
var gbm = new GBM(startPrice: 100, seed: 42);
for (int i = 0; i < count; i++) data[i] = gbm.Next().Close;
for (int i = 0; i < count; i++)
{
data[i] = gbm.Next().Close;
}
var output1 = new double[count];
var output2 = new double[count];
@@ -394,7 +413,10 @@ public class MamaTests
int count = 10;
var data = new double[count];
var gbm = new GBM(startPrice: 100, seed: 42);
for (int i = 0; i < count; i++) data[i] = gbm.Next().Close;
for (int i = 0; i < count; i++)
{
data[i] = gbm.Next().Close;
}
// Get values from span calculation
var mamaOutput = new double[count];
@@ -418,7 +440,10 @@ public class MamaTests
int count = 100;
var data = new double[count];
var gbm = new GBM(startPrice: 100, seed: 42);
for (int i = 0; i < count; i++) data[i] = gbm.Next().Close;
for (int i = 0; i < count; i++)
{
data[i] = gbm.Next().Close;
}
// 1. Streaming Mode (instance method)
var mama = new Mama();
+55 -12
View File
@@ -111,8 +111,16 @@ public sealed class Mama : AbstractBase
return 0.0; // Return neutral angle for invalid inputs
}
while (angle <= -Math.PI) angle += TwoPi;
while (angle > Math.PI) angle -= TwoPi;
while (angle <= -Math.PI)
{
angle += TwoPi;
}
while (angle > Math.PI)
{
angle -= TwoPi;
}
return angle;
}
@@ -192,11 +200,25 @@ public sealed class Mama : AbstractBase
double periodCap = _p_state.Period * 1.5;
double periodFloor = _p_state.Period * 0.67;
if (period > periodCap) period = periodCap;
if (period < periodFloor) period = periodFloor;
if (period > periodCap)
{
period = periodCap;
}
if (period < MinPeriod) period = MinPeriod;
if (period > MaxPeriod) period = MaxPeriod;
if (period < periodFloor)
{
period = periodFloor;
}
if (period < MinPeriod)
{
period = MinPeriod;
}
if (period > MaxPeriod)
{
period = MaxPeriod;
}
// Smooth Period (using FMA)
_state.Period = Math.FusedMultiplyAdd(SmoothCoef, period, SmoothPrev * _p_state.Period);
@@ -248,7 +270,10 @@ public sealed class Mama : AbstractBase
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return new TSeries([], []);
if (source.Count == 0)
{
return new TSeries([], []);
}
int len = source.Count;
var v = new List<double>(len);
@@ -307,7 +332,11 @@ public sealed class Mama : AbstractBase
throw new ArgumentOutOfRangeException(nameof(fastLimit), "FastLimit must be > SlowLimit");
}
if (source.Length == 0) return;
if (source.Length == 0)
{
return;
}
if (output.Length < source.Length)
{
throw new ArgumentOutOfRangeException(nameof(output), "Output buffer must be at least as large as the input buffer.");
@@ -427,11 +456,25 @@ public sealed class Mama : AbstractBase
double periodCap = p_period * 1.5;
double periodFloor = p_period * 0.67;
if (newPeriod > periodCap) newPeriod = periodCap;
if (newPeriod < periodFloor) newPeriod = periodFloor;
if (newPeriod > periodCap)
{
newPeriod = periodCap;
}
if (newPeriod < MinPeriod) newPeriod = MinPeriod;
if (newPeriod > MaxPeriod) newPeriod = MaxPeriod;
if (newPeriod < periodFloor)
{
newPeriod = periodFloor;
}
if (newPeriod < MinPeriod)
{
newPeriod = MinPeriod;
}
if (newPeriod > MaxPeriod)
{
newPeriod = MaxPeriod;
}
// Smooth Period (using FMA)
period = Math.FusedMultiplyAdd(SmoothCoef, newPeriod, SmoothPrev * p_period);
+20 -5
View File
@@ -33,7 +33,11 @@ public sealed class Mgdi : AbstractBase
public Mgdi(int period = 14, double k = 0.6)
{
ArgumentOutOfRangeException.ThrowIfLessThan(period, 1);
if (double.IsNaN(k) || double.IsInfinity(k) || k <= 0) throw new ArgumentOutOfRangeException(nameof(k), "k must be a finite value greater than 0");
if (double.IsNaN(k) || double.IsInfinity(k) || k <= 0)
{
throw new ArgumentOutOfRangeException(nameof(k), "k must be a finite value greater than 0");
}
_period = period;
_k = k;
Name = $"Mgdi({period},{k})";
@@ -123,7 +127,10 @@ public sealed class Mgdi : AbstractBase
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return new TSeries([], []);
if (source.Count == 0)
{
return new TSeries([], []);
}
int len = source.Count;
var t = new List<long>(len);
@@ -167,12 +174,20 @@ public sealed class Mgdi : AbstractBase
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period = 14, double k = 0.6)
{
ArgumentOutOfRangeException.ThrowIfLessThan(period, 1);
if (double.IsNaN(k) || double.IsInfinity(k) || k <= 0) throw new ArgumentOutOfRangeException(nameof(k), "k must be a finite value greater than 0");
if (double.IsNaN(k) || double.IsInfinity(k) || k <= 0)
{
throw new ArgumentOutOfRangeException(nameof(k), "k must be a finite value greater than 0");
}
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output must have the same length", nameof(output));
}
if (source.Length == 0) return;
if (source.Length == 0)
{
return;
}
double lastMgdi = 0;
double lastValid = 0;
@@ -225,4 +240,4 @@ public sealed class Mgdi : AbstractBase
{
Init();
}
}
}
@@ -68,9 +68,13 @@ public class MmaValidationTests
{
double val = source[i];
if (double.IsFinite(val))
{
lastValid = val;
}
else
{
val = lastValid;
}
if (double.IsNaN(val))
{
@@ -79,16 +83,22 @@ public class MmaValidationTests
}
if (count < window)
{
count++;
}
else
{
sum -= buffer[head];
}
buffer[head] = val;
sum += val;
head++;
if (head == window)
{
head = 0;
}
double sma = sum / count;
double weightedSum = ComputeWeightedSum(buffer, head, count);
@@ -101,7 +111,9 @@ public class MmaValidationTests
{
int idx = head - 1;
if (idx < 0)
{
idx = count - 1;
}
double weightedSum = 0.0;
for (int i = 0; i < count; i++)
@@ -111,7 +123,9 @@ public class MmaValidationTests
idx--;
if (idx < 0)
{
idx = count - 1;
}
}
return weightedSum;
+42 -3
View File
@@ -74,9 +74,13 @@ public sealed class Mma : AbstractBase
double val = input.Value;
if (double.IsFinite(val))
{
_lastValidValue = val;
}
else
{
val = _lastValidValue;
}
if (double.IsNaN(val))
{
@@ -97,7 +101,10 @@ public sealed class Mma : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
int len = source.Count;
var t = new List<long>(len);
@@ -123,9 +130,13 @@ public sealed class Mma : AbstractBase
{
double val = source.Values[i];
if (double.IsFinite(val))
{
lastValid = val;
}
else
{
val = lastValid;
}
if (double.IsNaN(val))
{
@@ -175,10 +186,16 @@ public sealed class Mma : AbstractBase
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period)
{
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output must have the same length.", nameof(output));
}
ArgumentOutOfRangeException.ThrowIfLessThan(period, 2);
if (source.Length == 0) return;
if (source.Length == 0)
{
return;
}
int window = Math.Min(Math.Max(2, period), MaxPeriod);
double sum = 0.0;
@@ -196,9 +213,13 @@ public sealed class Mma : AbstractBase
{
double val = source[i];
if (double.IsFinite(val))
{
lastValid = val;
}
else
{
val = lastValid;
}
if (double.IsNaN(val))
{
@@ -207,16 +228,22 @@ public sealed class Mma : AbstractBase
}
if (count < window)
{
count++;
}
else
{
sum -= buffer[head];
}
buffer[head] = val;
sum += val;
head++;
if (head == window)
{
head = 0;
}
double sma = sum / count;
double weightedSum = ComputeWeightedSum(buffer, head, count);
@@ -252,7 +279,9 @@ public sealed class Mma : AbstractBase
{
int count = _buffer.Count;
if (count <= 0)
{
return double.NaN;
}
double sma = _buffer.Sum / count;
double weightedSum = ComputeWeightedSum(_buffer, count);
@@ -260,7 +289,9 @@ public sealed class Mma : AbstractBase
double result = Math.FusedMultiplyAdd(weightedSum, 6.0 / denom, sma);
if (!state.IsHot && count >= _period)
{
state.IsHot = true;
}
return result;
}
@@ -274,7 +305,9 @@ public sealed class Mma : AbstractBase
int idx = start + count - 1;
if (idx >= capacity)
{
idx -= capacity;
}
double weightedSum = 0.0;
for (int i = 0; i < count; i++)
@@ -284,7 +317,9 @@ public sealed class Mma : AbstractBase
idx--;
if (idx < 0)
{
idx += capacity;
}
}
return weightedSum;
@@ -295,7 +330,9 @@ public sealed class Mma : AbstractBase
{
int idx = head - 1;
if (idx < 0)
{
idx = count - 1;
}
double weightedSum = 0.0;
for (int i = 0; i < count; i++)
@@ -305,9 +342,11 @@ public sealed class Mma : AbstractBase
idx--;
if (idx < 0)
{
idx = count - 1;
}
}
return weightedSum;
}
}
}
+10 -2
View File
@@ -350,7 +350,9 @@ public class QemaTests
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
for (int i = 0; i < source.Length; i++)
{
source[i] = gbm.Next().Close;
}
// Warm up
Qema.Batch(source.AsSpan(), output.AsSpan(), 100);
@@ -412,7 +414,10 @@ public class QemaTests
// Verify against a fresh QEMA fed with same data
var verifyQema = new Qema(5);
foreach (var val in history) verifyQema.Update(new TValue(DateTime.UtcNow, val));
foreach (var val in history)
{
verifyQema.Update(new TValue(DateTime.UtcNow, val));
}
Assert.Equal(verifyQema.Last.Value, qema.Last.Value, 1e-10);
Assert.Equal(verifyQema.IsHot, qema.IsHot);
@@ -432,7 +437,10 @@ public class QemaTests
qema.Prime(history);
var verifyQema = new Qema(5);
foreach (var val in history) verifyQema.Update(new TValue(DateTime.UtcNow, val));
foreach (var val in history)
{
verifyQema.Update(new TValue(DateTime.UtcNow, val));
}
Assert.Equal(verifyQema.Last.Value, qema.Last.Value, 1e-10);
}
+38 -4
View File
@@ -127,7 +127,10 @@ public sealed class Qema : AbstractBase
/// <param name="step">Optional time step (not used)</param>
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
if (source.Length == 0) return;
if (source.Length == 0)
{
return;
}
// Reset state
_state1 = EmaState.New();
@@ -163,9 +166,13 @@ public sealed class Qema : AbstractBase
{
double val = source[i];
if (double.IsFinite(val))
{
lastValid = val;
}
else
{
val = lastValid;
}
double e1 = ComputeEma(val, _alpha1, _decay1, ref s1);
double e2 = ComputeEma(e1, _alpha2, _decay2, ref s2);
@@ -200,7 +207,11 @@ public sealed class Qema : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double GetCompensated(EmaState s)
{
if (s.IsCompensated) return s.Ema;
if (s.IsCompensated)
{
return s.Ema;
}
return s.Ema / (1.0 - s.E);
}
@@ -226,9 +237,13 @@ public sealed class Qema : AbstractBase
double val = input.Value;
if (double.IsFinite(val))
{
_lastValidValue = val;
}
else
{
val = _lastValidValue;
}
// Cascaded EMAs
double e1 = ComputeEma(val, _alpha1, _decay1, ref _state1);
@@ -247,7 +262,10 @@ public sealed class Qema : AbstractBase
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
int len = source.Count;
List<long> t = new(len);
@@ -273,9 +291,13 @@ public sealed class Qema : AbstractBase
{
double val = sourceValues[i];
if (double.IsFinite(val))
{
lastValid = val;
}
else
{
val = lastValid;
}
double e1 = ComputeEma(val, _alpha1, _decay1, ref s1);
double e2 = ComputeEma(e1, _alpha2, _decay2, ref s2);
@@ -314,7 +336,9 @@ public sealed class Qema : AbstractBase
state.E *= decay;
if (!state.IsHot && state.E <= COVERAGE_THRESHOLD)
{
state.IsHot = true;
}
if (state.E <= COMPENSATOR_THRESHOLD)
{
@@ -402,10 +426,16 @@ public sealed class Qema : AbstractBase
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period)
{
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output must have the same length", nameof(output));
}
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(period);
if (source.Length == 0) return;
if (source.Length == 0)
{
return;
}
double alpha1 = Clamp01(2.0 / (period + 1));
double r = Math.Pow(1.0 / alpha1, 0.25);
@@ -446,9 +476,13 @@ public sealed class Qema : AbstractBase
{
double val = source[i];
if (double.IsFinite(val))
{
lastValid = val;
}
else
{
val = lastValid;
}
// EMA1
ema1_val = Math.FusedMultiplyAdd(ema1_val, decay1, alpha1 * val);
+1 -1
View File
@@ -204,4 +204,4 @@ public class RemaIndicatorTests
Assert.NotEqual(result1, result2);
}
}
}
+1 -1
View File
@@ -55,4 +55,4 @@ public class RemaIndicator : Indicator, IWatchlistIndicator
TValue result = ma.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), isNew: args.IsNewBar());
Series.SetValue(result.Value, ma.IsHot, ShowColdValues);
}
}
}
+20 -3
View File
@@ -447,7 +447,9 @@ public class RemaTests
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
for (int i = 0; i < source.Length; i++)
{
source[i] = gbm.Next().Close;
}
// Warm up
Rema.Batch(source.AsSpan(), output.AsSpan(), 100);
@@ -491,7 +493,10 @@ public class RemaTests
// Verify against a fresh REMA fed with same data
var verifyRema = new Rema(5);
foreach (var val in history) verifyRema.Update(new TValue(DateTime.UtcNow, val));
foreach (var val in history)
{
verifyRema.Update(new TValue(DateTime.UtcNow, val));
}
Assert.Equal(verifyRema.Last.Value, rema.Last.Value, 1e-10);
Assert.Equal(verifyRema.IsHot, rema.IsHot);
@@ -511,7 +516,10 @@ public class RemaTests
rema.Prime(history);
var verifyRema = new Rema(5);
foreach (var val in history) verifyRema.Update(new TValue(DateTime.UtcNow, val));
foreach (var val in history)
{
verifyRema.Update(new TValue(DateTime.UtcNow, val));
}
Assert.Equal(verifyRema.Last.Value, rema.Last.Value, 1e-10);
}
@@ -531,7 +539,10 @@ public class RemaTests
public void Calculate_ReturnsCorrectResultsAndHotIndicator()
{
var series = new TSeries();
for (int i = 1; i <= 20; i++) series.Add(DateTime.UtcNow, i * 10);
for (int i = 1; i <= 20; i++)
{
series.Add(DateTime.UtcNow, i * 10);
}
var (results, indicator) = Rema.Calculate(series, 5);
@@ -640,14 +651,20 @@ public class RemaTests
// 3. Streaming Mode
var streamingInd = new Rema(period, lambda);
for (int i = 0; i < series.Count; i++)
{
streamingInd.Update(series[i]);
}
double streamingResult = streamingInd.Last.Value;
// 4. Eventing Mode
var pubSource = new TSeries();
var eventingInd = new Rema(pubSource, period, lambda);
for (int i = 0; i < series.Count; i++)
{
pubSource.Add(series[i]);
}
double eventingResult = eventingInd.Last.Value;
Assert.Equal(expected, spanResult, precision: 9);
+9 -3
View File
@@ -304,7 +304,10 @@ public sealed class RemaValidationTests : IDisposable
private static double CalculateDiffVariance(double[] values, int startIdx, int count)
{
if (count < 2) return 0;
if (count < 2)
{
return 0;
}
// Calculate differences
double sumDiff = 0;
@@ -319,10 +322,13 @@ public sealed class RemaValidationTests : IDisposable
n++;
}
if (n < 2) return 0;
if (n < 2)
{
return 0;
}
double mean = sumDiff / n;
double variance = (sumDiffSq / n) - (mean * mean);
return Math.Max(0, variance); // Ensure non-negative due to floating point
}
}
}
+40 -7
View File
@@ -69,7 +69,9 @@ public sealed class Rema : AbstractBase
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(period);
if (lambda < 0.0 || lambda > 1.0)
{
throw new ArgumentOutOfRangeException(nameof(lambda), "Lambda must be between 0 and 1");
}
_alpha = 2.0 / (period + 1);
_decay = 1.0 - _alpha;
@@ -108,7 +110,10 @@ public sealed class Rema : AbstractBase
/// <inheritdoc/>
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
if (source.Length == 0) return;
if (source.Length == 0)
{
return;
}
_state = State.New();
_p_state = State.New();
@@ -152,7 +157,9 @@ public sealed class Rema : AbstractBase
finally
{
if (rented != null)
{
ArrayPool<double>.Shared.Return(rented);
}
}
}
@@ -196,7 +203,10 @@ public sealed class Rema : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
int len = source.Count;
var t = new List<long>(len);
@@ -245,7 +255,9 @@ public sealed class Rema : AbstractBase
state.E *= decay;
if (state.E <= COVERAGE_THRESHOLD)
{
state.IsHot = true;
}
result = input;
}
@@ -273,7 +285,9 @@ public sealed class Rema : AbstractBase
state.E *= decay;
if (!state.IsHot && state.E <= COVERAGE_THRESHOLD)
{
state.IsHot = true;
}
if (state.E <= COMPENSATOR_THRESHOLD)
{
@@ -311,9 +325,13 @@ public sealed class Rema : AbstractBase
{
double val = Unsafe.Add(ref srcRef, i);
if (!double.IsFinite(val))
{
val = lastValidValue;
}
else
{
lastValidValue = val;
}
double result;
@@ -326,7 +344,9 @@ public sealed class Rema : AbstractBase
state.E *= decay;
if (state.E <= COVERAGE_THRESHOLD)
{
state.IsHot = true;
}
result = val;
}
@@ -345,7 +365,9 @@ public sealed class Rema : AbstractBase
state.E *= decay;
if (!state.IsHot && state.E <= COVERAGE_THRESHOLD)
{
state.IsHot = true;
}
if (state.E <= COMPENSATOR_THRESHOLD)
{
@@ -399,13 +421,24 @@ public sealed class Rema : AbstractBase
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period, double lambda = 0.5)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
if (lambda < 0.0 || lambda > 1.0)
throw new ArgumentOutOfRangeException(nameof(lambda), "Lambda must be between 0 and 1");
if (source.Length != output.Length)
throw new ArgumentException("Source and output must have the same length", nameof(output));
}
if (source.Length == 0) return;
if (lambda < 0.0 || lambda > 1.0)
{
throw new ArgumentOutOfRangeException(nameof(lambda), "Lambda must be between 0 and 1");
}
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output must have the same length", nameof(output));
}
if (source.Length == 0)
{
return;
}
double alpha = 2.0 / (period + 1);
+6
View File
@@ -98,10 +98,14 @@ public class RgmaTests
var streamValues = new List<double>(series.Count);
for (int i = 0; i < series.Count; i++)
{
streamValues.Add(rgma.Update(series[i]).Value);
}
for (int i = 0; i < series.Count; i++)
{
Assert.Equal(batch[i].Value, streamValues[i], precision: 10);
}
}
[Fact]
@@ -117,7 +121,9 @@ public class RgmaTests
TSeries batch = Rgma.Batch(series, period, passes);
for (int i = 0; i < values.Length; i++)
{
Assert.Equal(batch[i].Value, output[i], precision: 10);
}
}
private static TSeries BuildSeries(int count, int seed)
@@ -27,12 +27,16 @@ public sealed class RgmaValidationTests : IDisposable
private void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
_disposed = true;
if (disposing)
{
_testData?.Dispose();
}
}
[Fact]
@@ -52,7 +56,9 @@ public sealed class RgmaValidationTests : IDisposable
int startIdx = rgmaResult.Count - compareCount;
for (int i = startIdx; i < rgmaResult.Count; i++)
{
Assert.Equal(emaResult[i].Value, rgmaResult[i].Value, 1e-10);
}
}
_output.WriteLine("RGMA(passes=1) Batch validated successfully against EMA");
@@ -81,7 +87,9 @@ public sealed class RgmaValidationTests : IDisposable
int startIdx = rgmaResults.Count - compareCount;
for (int i = startIdx; i < rgmaResults.Count; i++)
{
Assert.Equal(emaResults[i], rgmaResults[i], 1e-10);
}
}
_output.WriteLine("RGMA(passes=1) Streaming validated successfully against EMA");
@@ -105,7 +113,9 @@ public sealed class RgmaValidationTests : IDisposable
int startIdx = sourceData.Length - compareCount;
for (int i = startIdx; i < sourceData.Length; i++)
{
Assert.Equal(emaOutput[i], rgmaOutput[i], 1e-10);
}
}
_output.WriteLine("RGMA(passes=1) Span validated successfully against EMA");
@@ -131,7 +141,9 @@ public sealed class RgmaValidationTests : IDisposable
var rgmaStream = new Rgma(period, passCount);
var streaming = new double[_testData.Data.Count];
for (int i = 0; i < _testData.Data.Count; i++)
{
streaming[i] = rgmaStream.Update(_testData.Data[i]).Value;
}
// Span
var spanOutput = new double[sourceData.Length];
+58 -8
View File
@@ -111,7 +111,10 @@ public sealed class Rgma : AbstractBase
/// <inheritdoc/>
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
if (source.Length == 0) return;
if (source.Length == 0)
{
return;
}
_state = State.New();
_p_state = State.New();
@@ -172,9 +175,14 @@ public sealed class Rgma : AbstractBase
finally
{
if (filtersRented != null)
{
ArrayPool<double>.Shared.Return(filtersRented);
}
if (rented != null)
{
ArrayPool<double>.Shared.Return(rented);
}
}
}
@@ -189,7 +197,9 @@ public sealed class Rgma : AbstractBase
if (_passes <= 8)
{
for (int i = 0; i < _passes; i++)
{
_p_filters[i] = _filters[i];
}
}
else
{
@@ -203,7 +213,9 @@ public sealed class Rgma : AbstractBase
if (_passes <= 8)
{
for (int i = 0; i < _passes; i++)
{
_filters[i] = _p_filters[i];
}
}
else
{
@@ -222,7 +234,10 @@ public sealed class Rgma : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
int len = source.Count;
var t = new List<long>(len);
@@ -263,22 +278,31 @@ public sealed class Rgma : AbstractBase
state.TickCount = 1;
state.E *= decay;
if (state.E <= COVERAGE_THRESHOLD)
{
state.IsHot = true;
}
return input;
}
// Stage 0
filters[0] = Math.FusedMultiplyAdd(alpha, input - filters[0], filters[0]);
for (int i = 1; i < filters.Length; i++)
{
filters[i] = Math.FusedMultiplyAdd(alpha, filters[i - 1] - filters[i], filters[i]);
}
state.TickCount++;
state.E *= decay;
if (!state.IsHot && state.E <= COVERAGE_THRESHOLD)
{
state.IsHot = true;
}
if (state.TickCount >= ResyncInterval)
{
state.TickCount = 0;
}
return filters[^1];
}
@@ -298,9 +322,13 @@ public sealed class Rgma : AbstractBase
{
double x = source[i];
if (double.IsFinite(x))
{
lastValid = x;
}
else
{
x = lastValid;
}
double y;
if (!state.IsInitialized)
@@ -310,22 +338,31 @@ public sealed class Rgma : AbstractBase
state.TickCount = 1;
state.E *= decay;
if (state.E <= COVERAGE_THRESHOLD)
{
state.IsHot = true;
}
y = x;
}
else
{
filters[0] = Math.FusedMultiplyAdd(alpha, x - filters[0], filters[0]);
for (int p = 1; p < filters.Length; p++)
{
filters[p] = Math.FusedMultiplyAdd(alpha, filters[p - 1] - filters[p], filters[p]);
}
state.TickCount++;
state.E *= decay;
if (!state.IsHot && state.E <= COVERAGE_THRESHOLD)
{
state.IsHot = true;
}
if (state.TickCount >= ResyncInterval)
{
state.TickCount = 0;
}
y = filters[^1];
}
@@ -361,13 +398,24 @@ public sealed class Rgma : AbstractBase
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period, int passes = 3)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
if (passes <= 0)
throw new ArgumentException("Passes must be greater than 0", nameof(passes));
if (source.Length != output.Length)
throw new ArgumentException("Source and output must have the same length", nameof(output));
}
if (source.Length == 0) return;
if (passes <= 0)
{
throw new ArgumentException("Passes must be greater than 0", nameof(passes));
}
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output must have the same length", nameof(output));
}
if (source.Length == 0)
{
return;
}
double alpha = 2.0 / (period / Math.Sqrt(passes) + 1.0);
double decay = 1.0 - alpha;
@@ -405,7 +453,9 @@ public sealed class Rgma : AbstractBase
finally
{
if (rented != null)
{
ArrayPool<double>.Shared.Return(rented);
}
}
}
@@ -420,4 +470,4 @@ public sealed class Rgma : AbstractBase
Array.Fill(_p_filters, double.NaN);
Last = default;
}
}
}
+7 -1
View File
@@ -29,7 +29,9 @@ public sealed class Rma : AbstractBase
public Rma(int period)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
_ema = new Ema(1.0 / period);
Name = $"Rma({period})";
@@ -118,10 +120,14 @@ public sealed class Rma : AbstractBase
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
if (output.Length < source.Length)
{
throw new ArgumentException("Output span must be at least as long as source span", nameof(output));
}
double alpha = 1.0 / period;
Ema.Batch(source, output, alpha);
@@ -150,4 +156,4 @@ public sealed class Rma : AbstractBase
_ema.Reset();
Last = default;
}
}
}
+33 -4
View File
@@ -61,11 +61,19 @@ public sealed class T3 : AbstractBase
public T3(int period, double vfactor = 0.7)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
if (!double.IsFinite(vfactor))
{
throw new ArgumentOutOfRangeException(nameof(vfactor), "Volume factor must be a finite number (not NaN or Infinity)");
}
if (vfactor <= 0 || vfactor > 1)
{
throw new ArgumentOutOfRangeException(nameof(vfactor), "Volume factor must be greater than 0 and typically <= 1");
}
double alpha = 2.0 / (period + 1);
double decay = 1.0 - alpha;
@@ -136,7 +144,10 @@ public sealed class T3 : AbstractBase
/// <param name="source">Historical data</param>
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
if (source.Length == 0) return;
if (source.Length == 0)
{
return;
}
// Reset state
_state = State.New();
@@ -188,7 +199,7 @@ public sealed class T3 : AbstractBase
// So the state corresponds to "after processing source".
// To get the output value corresponding to the last input, we can calculate it from the state.
// But T3 formula uses the *updated* EMAs.
// T3 = c1*e6 + c2*e5 + c3*e4 + c4*e3
// T3 = c1*e6 + c2*e5 + c3*e4 + c4*e3
// The state has the updated EMAs.
double result = Math.FusedMultiplyAdd(_params.C4, _state.E3,
Math.FusedMultiplyAdd(_params.C3, _state.E4,
@@ -235,7 +246,10 @@ public sealed class T3 : AbstractBase
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
int len = source.Count;
var t = new List<long>(len);
@@ -298,9 +312,13 @@ public sealed class T3 : AbstractBase
{
double val = source[i];
if (double.IsFinite(val))
{
lastValidValue = val;
}
else
{
val = lastValidValue;
}
output[i] = Compute(val, p, ref state);
}
@@ -322,13 +340,24 @@ public sealed class T3 : AbstractBase
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period, double vfactor = 0.7)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output must have the same length", nameof(output));
}
if (!double.IsFinite(vfactor))
{
throw new ArgumentOutOfRangeException(nameof(vfactor), "Volume factor must be a finite number (not NaN or Infinity)");
}
if (vfactor <= 0 || vfactor > 1)
{
throw new ArgumentOutOfRangeException(nameof(vfactor), "Volume factor must be greater than 0 and typically <= 1");
}
double alpha = 2.0 / (period + 1);
double decay = 1.0 - alpha;
@@ -370,4 +399,4 @@ public sealed class T3 : AbstractBase
}
base.Dispose(disposing);
}
}
}
+2
View File
@@ -51,7 +51,9 @@ public sealed class TemaIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
if (args.Reason != UpdateReason.NewBar && args.Reason != UpdateReason.HistoricalBar && args.Reason != UpdateReason.NewTick)
{
return;
}
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
TValue result = _ma.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), args.IsNewBar());
+53 -9
View File
@@ -50,7 +50,10 @@ public sealed class Tema : AbstractBase
public Tema(int period)
{
if (period <= 0) throw new ArgumentException("Period must be greater than 0", nameof(period));
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
_alpha = 2.0 / (period + 1);
_decay = 1.0 - _alpha;
@@ -76,7 +79,10 @@ public sealed class Tema : AbstractBase
public Tema(double alpha)
{
if (alpha <= 0 || alpha >= 1) throw new ArgumentException("Alpha must be strictly between 0 and 1", nameof(alpha));
if (alpha <= 0 || alpha >= 1)
{
throw new ArgumentException("Alpha must be strictly between 0 and 1", nameof(alpha));
}
_alpha = alpha;
_decay = 1.0 - alpha;
@@ -93,7 +99,10 @@ public sealed class Tema : AbstractBase
/// <param name="source">Historical data</param>
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
if (source.Length == 0) return;
if (source.Length == 0)
{
return;
}
// Reset state
_state1 = EmaState.New();
@@ -131,9 +140,13 @@ public sealed class Tema : AbstractBase
{
double val = source[i];
if (double.IsFinite(val))
{
lastValid = val;
}
else
{
val = lastValid;
}
double e1 = Compute(val, alpha, decay, ref s1);
double e2 = Compute(e1, alpha, decay, ref s2);
@@ -157,7 +170,11 @@ public sealed class Tema : AbstractBase
double GetCompensated(EmaState s)
{
if (s.IsCompensated) return s.Ema;
if (s.IsCompensated)
{
return s.Ema;
}
return s.Ema / (1.0 - s.E);
}
@@ -196,9 +213,13 @@ public sealed class Tema : AbstractBase
// EMA1
double val = input.Value;
if (double.IsFinite(val))
{
_lastValidValue = val;
}
else
{
val = _lastValidValue;
}
double e1 = Compute(val, _alpha, _decay, ref _state1);
@@ -217,7 +238,10 @@ public sealed class Tema : AbstractBase
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
int len = source.Count;
List<long> t = new(len);
@@ -243,9 +267,13 @@ public sealed class Tema : AbstractBase
{
double val = sourceValues[i];
if (double.IsFinite(val))
{
lastValid = val;
}
else
{
val = lastValid;
}
double e1 = Compute(val, alpha, decay, ref s1);
double e2 = Compute(e1, alpha, decay, ref s2);
@@ -281,7 +309,9 @@ public sealed class Tema : AbstractBase
state.E *= decay;
if (!state.IsHot && state.E <= 0.05) // COVERAGE_THRESHOLD
{
state.IsHot = true;
}
if (state.E <= 1e-10) // COMPENSATOR_THRESHOLD
{
@@ -316,7 +346,9 @@ public sealed class Tema : AbstractBase
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
double alpha = 2.0 / (period + 1);
Batch(source, output, alpha);
@@ -325,11 +357,19 @@ public sealed class Tema : AbstractBase
public static void Batch(ReadOnlySpan<double> source, Span<double> output, double alpha)
{
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output must have the same length", nameof(output));
if (alpha <= 0 || alpha >= 1)
throw new ArgumentException("Alpha must be strictly between 0 and 1", nameof(alpha));
}
if (source.Length == 0) return;
if (alpha <= 0 || alpha >= 1)
{
throw new ArgumentException("Alpha must be strictly between 0 and 1", nameof(alpha));
}
if (source.Length == 0)
{
return;
}
double decay = 1.0 - alpha;
double lastValid = 0;
@@ -363,9 +403,13 @@ public sealed class Tema : AbstractBase
{
double val = source[i];
if (double.IsFinite(val))
{
lastValid = val;
}
else
{
val = lastValid;
}
// Update EMA1: ema = decay * ema + alpha * input = FMA(decay, ema, alpha * input)
ema1_val = Math.FusedMultiplyAdd(decay, ema1_val, alpha * val);
@@ -447,4 +491,4 @@ public sealed class Tema : AbstractBase
_p_lastValidValue = 0;
Last = default;
}
}
}
+8 -2
View File
@@ -296,7 +296,10 @@ public class VamaTests
// Verify against a fresh VAMA fed with same data
var verifyVama = new Vama();
foreach (var val in history) verifyVama.Update(new TValue(DateTime.UtcNow, val));
foreach (var val in history)
{
verifyVama.Update(new TValue(DateTime.UtcNow, val));
}
Assert.Equal(verifyVama.Last.Value, vama.Last.Value, 1e-10);
@@ -315,7 +318,10 @@ public class VamaTests
vama.Prime(history);
var verifyVama = new Vama();
foreach (var val in history) verifyVama.Update(new TValue(DateTime.UtcNow, val));
foreach (var val in history)
{
verifyVama.Update(new TValue(DateTime.UtcNow, val));
}
Assert.Equal(verifyVama.Last.Value, vama.Last.Value, 1e-10);
}
+37 -4
View File
@@ -77,17 +77,34 @@ public sealed class Vama : AbstractBase
public Vama(int baseLength = 20, int shortAtrPeriod = 10, int longAtrPeriod = 50, int minLength = 5, int maxLength = 100)
{
if (baseLength <= 0)
{
throw new ArgumentException("Base length must be greater than 0", nameof(baseLength));
}
if (shortAtrPeriod <= 0)
{
throw new ArgumentException("Short ATR period must be greater than 0", nameof(shortAtrPeriod));
}
if (longAtrPeriod <= 0)
{
throw new ArgumentException("Long ATR period must be greater than 0", nameof(longAtrPeriod));
}
if (minLength <= 0)
{
throw new ArgumentException("Min length must be greater than 0", nameof(minLength));
}
if (maxLength <= 0)
{
throw new ArgumentException("Max length must be greater than 0", nameof(maxLength));
}
if (minLength > maxLength)
{
throw new ArgumentException("Min length must be less than or equal to max length", nameof(minLength));
}
_baseLength = baseLength;
_minLength = minLength;
@@ -167,11 +184,17 @@ public sealed class Vama : AbstractBase
shortAtr.Ema = Math.FusedMultiplyAdd(shortAtr.Ema, _shortDecay, _shortAlpha * trueRange);
shortAtr.E *= _shortDecay;
if (shortAtr.E <= EPSILON) shortAtr.IsCompensated = true;
if (shortAtr.E <= EPSILON)
{
shortAtr.IsCompensated = true;
}
longAtr.Ema = Math.FusedMultiplyAdd(longAtr.Ema, _longDecay, _longAlpha * trueRange);
longAtr.E *= _longDecay;
if (longAtr.E <= EPSILON) longAtr.IsCompensated = true;
if (longAtr.E <= EPSILON)
{
longAtr.IsCompensated = true;
}
// Compensated ATR values
double shortAtrValue = shortAtr.IsCompensated ? shortAtr.Ema : shortAtr.Ema / (1.0 - shortAtr.E);
@@ -187,9 +210,13 @@ public sealed class Vama : AbstractBase
// Update circular buffer with source value
double sourceValue = input.Close;
if (!double.IsFinite(sourceValue))
{
sourceValue = _lastValidValue;
}
else
{
_lastValidValue = sourceValue;
}
// Remove oldest value from sum if it was valid
double oldest = _buffer[_state.BufferHead];
@@ -271,7 +298,10 @@ public sealed class Vama : AbstractBase
/// </summary>
public TSeries Update(TBarSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
int len = source.Count;
var t = new List<long>(len);
@@ -298,7 +328,10 @@ public sealed class Vama : AbstractBase
/// </summary>
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
int len = source.Count;
var t = new List<long>(len);
+2
View File
@@ -51,7 +51,9 @@ public sealed class VidyaIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
if (args.Reason != UpdateReason.NewBar && args.Reason != UpdateReason.HistoricalBar && args.Reason != UpdateReason.NewTick)
{
return;
}
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
TValue result = _ma.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), args.IsNewBar());
+34 -8
View File
@@ -46,7 +46,9 @@ public sealed class Vidya : AbstractBase
public Vidya(int period)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
_period = period;
_alpha = 2.0 / (period + 1);
@@ -100,7 +102,11 @@ public sealed class Vidya : AbstractBase
double price = input.Value;
if (!double.IsFinite(price))
{
if (!_state.IsInitialized) return input;
if (!_state.IsInitialized)
{
return input;
}
price = _state.CurrentClose;
}
@@ -147,7 +153,10 @@ public sealed class Vidya : AbstractBase
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
int len = source.Count;
var t = new List<long>(len);
@@ -197,7 +206,10 @@ public sealed class Vidya : AbstractBase
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
if (source.Length == 0) return;
if (source.Length == 0)
{
return;
}
// Reset state
Reset();
@@ -224,7 +236,10 @@ public sealed class Vidya : AbstractBase
for (int i = 1; i < source.Length; i++)
{
double price = source[i];
if (!double.IsFinite(price)) price = prevClose;
if (!double.IsFinite(price))
{
price = prevClose;
}
double change = price - prevClose;
double up = change > 0 ? change : 0;
@@ -295,11 +310,19 @@ public sealed class Vidya : AbstractBase
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
if (source.Length != output.Length)
throw new ArgumentException("Source and output must have the same length", nameof(output));
}
if (source.Length == 0) return;
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output must have the same length", nameof(output));
}
if (source.Length == 0)
{
return;
}
double alpha = 2.0 / (period + 1);
@@ -346,7 +369,10 @@ public sealed class Vidya : AbstractBase
sumDown += down;
head++;
if (head >= period) head = 0;
if (head >= period)
{
head = 0;
}
double sum = sumUp + sumDown;
double vi = 0;
+2
View File
@@ -70,7 +70,9 @@ public class YzvamaTests
Assert.False(yzvama.IsHot);
for (int i = 0; i < 5; i++)
{
yzvama.Update(new TValue(DateTime.UtcNow, 100 + i), isNew: true);
}
Assert.True(yzvama.IsHot);
}
@@ -35,7 +35,9 @@ public class YzvamaValidationTests
const double constantValue = 42.5;
for (int i = 0; i < 300; i++)
{
yzvama.Update(new TValue(DateTime.UtcNow, constantValue), isNew: true);
}
Assert.Equal(constantValue, yzvama.Last.Value, Tolerance);
}
+47 -4
View File
@@ -89,17 +89,34 @@ public sealed class Yzvama : AbstractBase
public Yzvama(int yzvShortPeriod = 3, int yzvLongPeriod = 50, int percentileLookback = 100, int minLength = 5, int maxLength = 100)
{
if (yzvShortPeriod <= 0)
{
throw new ArgumentException("Short YZV period must be greater than 0", nameof(yzvShortPeriod));
}
if (yzvLongPeriod <= 0)
{
throw new ArgumentException("Long YZV period must be greater than 0", nameof(yzvLongPeriod));
}
if (percentileLookback <= 0)
{
throw new ArgumentException("Percentile lookback must be greater than 0", nameof(percentileLookback));
}
if (minLength <= 0)
{
throw new ArgumentException("Min length must be greater than 0", nameof(minLength));
}
if (maxLength <= 0)
{
throw new ArgumentException("Max length must be greater than 0", nameof(maxLength));
}
if (minLength > maxLength)
{
throw new ArgumentException("Min length must be less than or equal to max length", nameof(minLength));
}
_percentileLookback = percentileLookback;
_minLength = minLength;
@@ -163,7 +180,9 @@ public sealed class Yzvama : AbstractBase
private static double ComputeYangZhangK(int period)
{
if (period <= 1)
{
return 0.34 / (1.34 + 1.0);
}
double ratioN = (period + 1.0) / (period - 1.0);
return 0.34 / (1.34 + ratioN);
@@ -178,9 +197,13 @@ public sealed class Yzvama : AbstractBase
{
int mid = lo + ((hi - lo) >> 1);
if (sorted[mid] < value)
{
lo = mid + 1;
}
else
{
hi = mid;
}
}
return lo;
}
@@ -254,9 +277,13 @@ public sealed class Yzvama : AbstractBase
// Sanitize source
if (!double.IsFinite(sourceValue))
{
sourceValue = double.IsFinite(_lastValidSource) ? _lastValidSource : 0.0;
}
else
{
_lastValidSource = sourceValue;
}
// Compute Yang-Zhang variance components (log returns)
double yzvShort = double.NaN;
@@ -290,7 +317,10 @@ public sealed class Yzvama : AbstractBase
// Update short RMA variance
shortVar.Ema = Math.FusedMultiplyAdd(shortVar.Ema, _shortDecay, _shortAlpha * sSqDailyShort);
shortVar.E *= _shortDecay;
if (shortVar.E <= EPSILON) shortVar.IsCompensated = true;
if (shortVar.E <= EPSILON)
{
shortVar.IsCompensated = true;
}
double shortVarValue = shortVar.IsCompensated ? shortVar.Ema : shortVar.Ema / (1.0 - shortVar.E);
yzvShort = shortVarValue >= 0 ? Math.Sqrt(shortVarValue) : double.NaN;
@@ -298,7 +328,10 @@ public sealed class Yzvama : AbstractBase
// Update long RMA variance (kept for parity with Pine implementation)
longVar.Ema = Math.FusedMultiplyAdd(longVar.Ema, _longDecay, _longAlpha * sSqDailyLong);
longVar.E *= _longDecay;
if (longVar.E <= EPSILON) longVar.IsCompensated = true;
if (longVar.E <= EPSILON)
{
longVar.IsCompensated = true;
}
}
}
@@ -414,7 +447,10 @@ public sealed class Yzvama : AbstractBase
/// </summary>
public TSeries Update(TBarSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
int len = source.Count;
var t = new List<long>(len);
@@ -441,7 +477,10 @@ public sealed class Yzvama : AbstractBase
/// </summary>
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
int len = source.Count;
var t = new List<long>(len);
@@ -473,7 +512,9 @@ public sealed class Yzvama : AbstractBase
{
Reset();
foreach (double val in source)
{
Update(new TValue(DateTime.MinValue, val), isNew: true);
}
}
/// <summary>
@@ -484,7 +525,9 @@ public sealed class Yzvama : AbstractBase
{
Reset();
foreach (TValue tv in source)
{
Update(tv, isNew: true);
}
}
/// <summary>
@@ -74,9 +74,13 @@ public class ZlemaValidationTests
{
double val = source[i];
if (double.IsFinite(val))
{
lastValid = val;
}
else
{
val = lastValid;
}
if (double.IsNaN(val))
{
@@ -87,7 +91,9 @@ public class ZlemaValidationTests
buffer[head] = val;
head++;
if (head == bufferSize)
{
head = 0;
}
double lagged = buffer[head];
double signal = Math.FusedMultiplyAdd(2.0, val, -lagged);
+47 -5
View File
@@ -58,7 +58,9 @@ public sealed class Zlema : AbstractBase
public Zlema(double alpha)
{
if (alpha <= 0.0 || alpha > 1.0 || !double.IsFinite(alpha))
{
throw new ArgumentException("Alpha must be finite and in (0, 1].", nameof(alpha));
}
_alpha = alpha;
_beta = 1.0 - _alpha;
@@ -97,9 +99,13 @@ public sealed class Zlema : AbstractBase
double val = input.Value;
if (double.IsFinite(val))
{
_lastValidValue = val;
}
else
{
val = _lastValidValue;
}
if (double.IsNaN(val))
{
@@ -123,7 +129,10 @@ public sealed class Zlema : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
int len = source.Count;
List<long> t = new(len);
@@ -146,9 +155,13 @@ public sealed class Zlema : AbstractBase
{
double val = source.Values[i];
if (double.IsFinite(val))
{
lastValid = val;
}
else
{
val = lastValid;
}
if (double.IsNaN(val))
{
@@ -190,10 +203,16 @@ public sealed class Zlema : AbstractBase
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period)
{
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output must have the same length.", nameof(output));
}
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(period);
if (source.Length == 0) return;
if (source.Length == 0)
{
return;
}
double alpha = 2.0 / (period + 1);
Calculate(source, output, alpha, period);
@@ -202,11 +221,19 @@ public sealed class Zlema : AbstractBase
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, double alpha)
{
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output must have the same length.", nameof(output));
if (alpha <= 0.0 || alpha > 1.0 || !double.IsFinite(alpha))
throw new ArgumentException("Alpha must be finite and in (0, 1].", nameof(alpha));
}
if (source.Length == 0) return;
if (alpha <= 0.0 || alpha > 1.0 || !double.IsFinite(alpha))
{
throw new ArgumentException("Alpha must be finite and in (0, 1].", nameof(alpha));
}
if (source.Length == 0)
{
return;
}
double period = (2.0 / alpha) - 1.0;
Calculate(source, output, alpha, period);
@@ -231,7 +258,9 @@ public sealed class Zlema : AbstractBase
state.E *= _beta;
if (!state.IsHot && state.Bars >= _lag + 1 && state.E <= CoverageThreshold)
{
state.IsHot = true;
}
if (state.E <= CompensatorThreshold)
{
@@ -246,7 +275,10 @@ public sealed class Zlema : AbstractBase
else
{
if (!state.IsHot && state.Bars >= _lag + 1)
{
state.IsHot = true;
}
result = state.ZlemaRaw;
}
@@ -257,11 +289,15 @@ public sealed class Zlema : AbstractBase
private static int EstimateWarmupPeriod(double beta)
{
if (beta <= 0.0)
{
return 1;
}
double steps = Math.Log(CoverageThreshold) / Math.Log(beta);
if (double.IsNaN(steps) || double.IsInfinity(steps) || steps <= 0.0)
{
return 1;
}
return (int)Math.Ceiling(steps);
}
@@ -317,9 +353,13 @@ public sealed class Zlema : AbstractBase
{
double val = source[i];
if (double.IsFinite(val))
{
lastValid = val;
}
else
{
val = lastValid;
}
if (double.IsNaN(val))
{
@@ -330,7 +370,9 @@ public sealed class Zlema : AbstractBase
buffer[head] = val;
head++;
if (head == bufferSize)
{
head = 0;
}
double lagged = buffer[head];
double signal = Math.FusedMultiplyAdd(2.0, val, -lagged);