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
+10 -1
View File
@@ -34,7 +34,9 @@ public class Atan2ValidationTests
private static double PineScriptAtan2(double y, double x)
{
if (y == 0.0 && x == 0.0)
{
throw new ArgumentException("atan2: Both y and x cannot be zero", nameof(y));
}
double ay = Math.Abs(y);
double ax = Math.Abs(x);
@@ -50,9 +52,14 @@ public class Atan2ValidationTests
}
if (x < 0.0)
{
angle = Math.PI - angle;
}
if (y < 0.0)
{
angle = -angle;
}
return angle;
}
@@ -303,7 +310,9 @@ public class Atan2ValidationTests
// Skip origin (angle = 0 with x=1, y=0 is fine, but need to handle numerical zeros)
if (Math.Abs(x) < 1e-15 && Math.Abs(y) < 1e-15)
{
continue;
}
double dotNet = Math.Atan2(y, x);
double pine = PineScriptAtan2(y, x);
@@ -348,4 +357,4 @@ public class Atan2ValidationTests
Assert.True(true); // This is a documentation test
}
}
}
+17 -4
View File
@@ -47,7 +47,10 @@ public class AccelIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
if (_accel == null || _selector == null) return;
if (_accel == null || _selector == null)
{
return;
}
var item = HistoricalData[0, SeekOriginHistory.End];
double value = _selector(item);
@@ -64,7 +67,9 @@ public class AccelIndicator : Indicator, IWatchlistIndicator
{
value = _accel!.Last.Value;
if (!double.IsFinite(value))
{
value = 0.0;
}
}
TValue input = new(time, value);
@@ -87,8 +92,16 @@ public class AccelIndicator : Indicator, IWatchlistIndicator
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static IndicatorLineMarker GetMarker(double value)
{
if (value > 0) return GreenMarker;
if (value < 0) return RedMarker;
if (value > 0)
{
return GreenMarker;
}
if (value < 0)
{
return RedMarker;
}
return GrayMarker;
}
}
}
+2 -2
View File
@@ -254,11 +254,11 @@ public class AccelTests
{
var source = new TSeries();
var accel = new Accel(source);
source.Add(new TValue(DateTime.UtcNow, 10));
source.Add(new TValue(DateTime.UtcNow, 20));
source.Add(new TValue(DateTime.UtcNow, 35));
Assert.True(accel.IsHot);
Assert.Equal(5, accel.Last.Value); // 35 - 2*20 + 10 = 5
}
+48 -10
View File
@@ -110,7 +110,10 @@ public sealed class Accel : AbstractBase
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
int len = source.Count;
@@ -187,16 +190,28 @@ public sealed class Accel : 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));
}
int len = source.Length;
if (len == 0) return;
if (len == 0)
{
return;
}
// First two elements have insufficient history
output[0] = 0.0;
if (len == 1) return;
if (len == 1)
{
return;
}
output[1] = 0.0;
if (len == 2) return;
if (len == 2)
{
return;
}
int i = 2;
@@ -279,9 +294,20 @@ public sealed class Accel : AbstractBase
// Handle NaN/Infinity by substitution (find first finite value)
double fallback = FindFinite(curr, p1, p2);
if (!double.IsFinite(curr)) curr = fallback;
if (!double.IsFinite(p1)) p1 = fallback;
if (!double.IsFinite(p2)) p2 = fallback;
if (!double.IsFinite(curr))
{
curr = fallback;
}
if (!double.IsFinite(p1))
{
p1 = fallback;
}
if (!double.IsFinite(p2))
{
p2 = fallback;
}
// accel = curr - 2*prev1 + prev2
output[i] = Math.FusedMultiplyAdd(-2.0, p1, curr + p2);
@@ -291,9 +317,21 @@ public sealed class Accel : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double FindFinite(double a, double b, double c)
{
if (double.IsFinite(a)) return a;
if (double.IsFinite(b)) return b;
if (double.IsFinite(c)) return c;
if (double.IsFinite(a))
{
return a;
}
if (double.IsFinite(b))
{
return b;
}
if (double.IsFinite(c))
{
return c;
}
return 0.0;
}
}
+19 -4
View File
@@ -50,7 +50,10 @@ public class ChangeIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
if (_change == null || _selector == null) return;
if (_change == null || _selector == null)
{
return;
}
var item = HistoricalData[0, SeekOriginHistory.End];
double value = _selector(item);
@@ -76,9 +79,21 @@ public class ChangeIndicator : Indicator, IWatchlistIndicator
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
private static IndicatorLineMarker GetMarker(double value)
{
if (!double.IsFinite(value)) return GrayMarker;
if (value > 0) return GreenMarker;
if (value < 0) return RedMarker;
if (!double.IsFinite(value))
{
return GrayMarker;
}
if (value > 0)
{
return GreenMarker;
}
if (value < 0)
{
return RedMarker;
}
return GrayMarker;
}
}
+1 -1
View File
@@ -220,4 +220,4 @@ public class ChangeTests
Assert.True(change.IsHot);
Assert.NotEqual(0.0, change.Last.Value);
}
}
}
+16
View File
@@ -34,7 +34,9 @@ public sealed class Change : AbstractBase
public Change(int period = 1)
{
if (period < 1)
{
throw new ArgumentException("Period must be >= 1", nameof(period));
}
_period = period;
_buffer = new RingBuffer(period + 1);
@@ -59,9 +61,13 @@ public sealed class Change : AbstractBase
public override TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
_p_state = _state;
}
else
{
_state = _p_state;
}
double value = double.IsFinite(input.Value) ? input.Value : _state.LastValid;
_state = new State(value);
@@ -122,11 +128,19 @@ public sealed class Change : AbstractBase
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period = 1)
{
if (source.Length == 0)
{
throw new ArgumentException("Source cannot be empty", nameof(source));
}
if (output.Length < source.Length)
{
throw new ArgumentException("Output length must be >= source length", nameof(output));
}
if (period < 1)
{
throw new ArgumentException("Period must be >= 1", nameof(period));
}
// Use ArrayPool for large periods to track past valid values
const int StackAllocThreshold = 256;
@@ -184,7 +198,9 @@ public sealed class Change : AbstractBase
finally
{
if (pastValidRented != null)
{
System.Buffers.ArrayPool<double>.Shared.Return(pastValidRented);
}
}
}
+5 -2
View File
@@ -40,7 +40,10 @@ public class ExptransIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
if (_exptrans == null || _selector == null) return;
if (_exptrans == null || _selector == null)
{
return;
}
var item = HistoricalData[0, SeekOriginHistory.End];
double value = _selector(item);
@@ -53,4 +56,4 @@ public class ExptransIndicator : Indicator, IWatchlistIndicator
LinesSeries[0].SetValue(_exptrans.Last.Value, isHot, ShowColdValues);
}
}
}
+1 -1
View File
@@ -274,4 +274,4 @@ public class ExptransTests
Assert.True(indicator.Last.Value > 0);
}
}
}
}
@@ -170,4 +170,4 @@ public class ExptransValidationTests
Assert.Equal(Math.Pow(expA, n), expNA, 1e-12);
}
}
}
+9
View File
@@ -46,9 +46,13 @@ public sealed class Exptrans : AbstractBase
public override TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
_p_state = _state;
}
else
{
_state = _p_state;
}
double value = input.Value;
double result;
@@ -114,9 +118,14 @@ public sealed class Exptrans : AbstractBase
public static void Calculate(ReadOnlySpan<double> source, Span<double> output)
{
if (source.Length == 0)
{
throw new ArgumentException("Source cannot be empty", nameof(source));
}
if (output.Length < source.Length)
{
throw new ArgumentException("Output length must be >= source length", nameof(output));
}
double lastValid = 1.0; // exp(0) = 1
+4 -1
View File
@@ -43,7 +43,10 @@ public class HighestIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
if (_highest == null || _selector == null) return;
if (_highest == null || _selector == null)
{
return;
}
var item = HistoricalData[0, SeekOriginHistory.End];
double value = _selector(item);
+1 -1
View File
@@ -297,4 +297,4 @@ public class HighestTests
indicator.Update(new TValue(time.AddMinutes(5), 5.0));
Assert.Equal(9.0, indicator.Last.Value, Tolerance);
}
}
}
+34 -3
View File
@@ -33,7 +33,9 @@ public sealed class Highest : AbstractBase
public Highest(int period)
{
if (period < 1)
{
throw new ArgumentException("Period must be >= 1", nameof(period));
}
_period = period;
_buffer = new RingBuffer(period);
@@ -58,9 +60,13 @@ public sealed class Highest : AbstractBase
public override TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
_p_state = _state;
}
else
{
_state = _p_state;
}
double value = double.IsFinite(input.Value) ? input.Value : _state.LastValid;
_state = new State(value);
@@ -112,11 +118,19 @@ public sealed class Highest : AbstractBase
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period)
{
if (source.Length == 0)
{
throw new ArgumentException("Source cannot be empty", nameof(source));
}
if (output.Length < source.Length)
{
throw new ArgumentException("Output length must be >= source length", nameof(output));
}
if (period < 1)
{
throw new ArgumentException("Period must be >= 1", nameof(period));
}
int len = source.Length;
@@ -169,7 +183,11 @@ public sealed class Highest : AbstractBase
while (count > 0 && deque[head] <= i - period)
{
head++;
if (head >= capacity) head -= capacity;
if (head >= capacity)
{
head -= capacity;
}
count--;
}
@@ -177,7 +195,11 @@ public sealed class Highest : AbstractBase
while (count > 0)
{
int backIdx = tail - 1;
if (backIdx < 0) backIdx += capacity;
if (backIdx < 0)
{
backIdx += capacity;
}
if (values[deque[backIdx]] <= value)
{
tail = backIdx;
@@ -192,7 +214,11 @@ public sealed class Highest : AbstractBase
// Add current index at tail
deque[tail] = i;
tail++;
if (tail >= capacity) tail -= capacity;
if (tail >= capacity)
{
tail -= capacity;
}
count++;
output[i] = values[deque[head]];
@@ -201,9 +227,14 @@ public sealed class Highest : AbstractBase
finally
{
if (rentedDeque != null)
{
System.Buffers.ArrayPool<int>.Shared.Return(rentedDeque);
}
if (rentedValues != null)
{
System.Buffers.ArrayPool<double>.Shared.Return(rentedValues);
}
}
}
+4 -4
View File
@@ -184,8 +184,8 @@ public class JerkIndicatorTests
var now = DateTime.UtcNow;
// Cubic trend: f(x) = x³ has third derivative = 6
// Using f(i) = i³, the discrete third differences converge to 6
// Cubic trend: f(x) = x³ has third derivative = 6
// Using f(i) = i³, the discrete third differences converge to 6
for (int i = 0; i < 10; i++)
{
double price = 100 + i * i * i; // cubic growth
@@ -194,7 +194,7 @@ public class JerkIndicatorTests
}
double lastJerk = indicator.LinesSeries[0].GetValue(0);
// For f(x) = x³, discrete third difference = 6
// For f(x) = x³, discrete third difference = 6
Assert.Equal(6.0, lastJerk, 6);
}
@@ -217,4 +217,4 @@ public class JerkIndicatorTests
double lastJerk = indicator.LinesSeries[0].GetValue(0);
Assert.Equal(0, lastJerk, 6);
}
}
}
+11 -1
View File
@@ -41,7 +41,10 @@ public class JerkIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
if (_jerk == null || _selector == null) return;
if (_jerk == null || _selector == null)
{
return;
}
var item = HistoricalData[0, SeekOriginHistory.End];
double value = _selector(item);
@@ -60,11 +63,18 @@ public class JerkIndicator : Indicator, IWatchlistIndicator
double jerk = _jerk.Last.Value;
Color color;
if (jerk > 0)
{
color = Color.Green;
}
else if (jerk < 0)
{
color = Color.Red;
}
else
{
color = Color.Gray;
}
LinesSeries[0].SetMarker(0, new IndicatorLineMarker(color));
}
}
+3 -3
View File
@@ -258,12 +258,12 @@ public class JerkTests
{
var source = new TSeries();
var jerk = new Jerk(source);
source.Add(new TValue(DateTime.UtcNow, 10));
source.Add(new TValue(DateTime.UtcNow, 20));
source.Add(new TValue(DateTime.UtcNow, 35));
source.Add(new TValue(DateTime.UtcNow, 40));
Assert.True(jerk.IsHot);
// jerk = 40 - 3*35 + 3*20 - 10 = 40 - 105 + 60 - 10 = -15
Assert.Equal(-15, jerk.Last.Value);
@@ -304,4 +304,4 @@ public class JerkTests
Assert.Equal(jerkResults[i], chainResults[i], precision: 9);
}
}
}
}
+66 -16
View File
@@ -78,8 +78,8 @@ public sealed class Jerk : AbstractBase
if (_state.Count >= 3)
{
// jerk = val - 3*prev1 + 3*prev2 - prev3
// Using FMA: val - 3*prev1 + 3*prev2 - prev3
// jerk = val - 3*prev1 + 3*prev2 - prev3
// Using FMA: val - 3*prev1 + 3*prev2 - prev3
// = FMA(-3, prev1, val) + FMA(3, prev2, -prev3)
double term1 = Math.FusedMultiplyAdd(-3.0, _state.Prev1, val);
double term2 = Math.FusedMultiplyAdd(3.0, _state.Prev2, -_state.Prev3);
@@ -127,7 +127,10 @@ public sealed class Jerk : AbstractBase
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
int len = source.Count;
@@ -211,18 +214,34 @@ public sealed class Jerk : 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));
}
int len = source.Length;
if (len == 0) return;
if (len == 0)
{
return;
}
// First three elements have insufficient history
output[0] = 0.0;
if (len == 1) return;
if (len == 1)
{
return;
}
output[1] = 0.0;
if (len == 2) return;
if (len == 2)
{
return;
}
output[2] = 0.0;
if (len == 3) return;
if (len == 3)
{
return;
}
int i = 3;
@@ -339,10 +358,25 @@ public sealed class Jerk : AbstractBase
// Handle NaN/Infinity by substitution (find first finite value)
double fallback = FindFinite(curr, p1, p2, p3);
if (!double.IsFinite(curr)) curr = fallback;
if (!double.IsFinite(p1)) p1 = fallback;
if (!double.IsFinite(p2)) p2 = fallback;
if (!double.IsFinite(p3)) p3 = fallback;
if (!double.IsFinite(curr))
{
curr = fallback;
}
if (!double.IsFinite(p1))
{
p1 = fallback;
}
if (!double.IsFinite(p2))
{
p2 = fallback;
}
if (!double.IsFinite(p3))
{
p3 = fallback;
}
// jerk = curr - 3*prev1 + 3*prev2 - prev3
double term1 = Math.FusedMultiplyAdd(-3.0, p1, curr);
@@ -354,10 +388,26 @@ public sealed class Jerk : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double FindFinite(double a, double b, double c, double d)
{
if (double.IsFinite(a)) return a;
if (double.IsFinite(b)) return b;
if (double.IsFinite(c)) return c;
if (double.IsFinite(d)) return d;
if (double.IsFinite(a))
{
return a;
}
if (double.IsFinite(b))
{
return b;
}
if (double.IsFinite(c))
{
return c;
}
if (double.IsFinite(d))
{
return d;
}
return 0.0;
}
}
}
@@ -134,4 +134,4 @@ public class LineartransIndicatorTests
// Identity transform: 1.0 * 42.5 + 0.0 = 42.5
Assert.Equal(42.5, indicator.LinesSeries[0].GetValue(0), 1e-10);
}
}
}
@@ -46,7 +46,10 @@ public class LineartransIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
if (_lineartrans == null || _selector == null) return;
if (_lineartrans == null || _selector == null)
{
return;
}
var item = HistoricalData[0, SeekOriginHistory.End];
double value = _selector(item);
@@ -59,4 +62,4 @@ public class LineartransIndicator : Indicator, IWatchlistIndicator
LinesSeries[0].SetValue(_lineartrans.Last.Value, isHot, ShowColdValues);
}
}
}
@@ -172,7 +172,9 @@ public class LineartransTests
var time = DateTime.UtcNow;
for (int i = 0; i < 5; i++)
{
series.Add(new TValue(time.AddSeconds(i), i * 10.0), true);
}
var result = linear.Update(series);
@@ -191,7 +193,9 @@ public class LineartransTests
var time = DateTime.UtcNow;
for (int i = 0; i < 3; i++)
{
series.Add(new TValue(time.AddSeconds(i), 10.0 * (i + 1)), true);
}
var result = Lineartrans.Calculate(series, slope: 0.5, intercept: 5.0);
@@ -257,7 +261,9 @@ public class LineartransTests
var streamIndicator = new Lineartrans(slope, intercept);
var streamResult = new TSeries();
for (int i = 0; i < series.Count; i++)
{
streamResult.Add(streamIndicator.Update(series[i], true), true);
}
// Span
var spanOutput = new double[series.Count];
@@ -270,4 +276,4 @@ public class LineartransTests
Assert.Equal(batchResult[i].Value, spanOutput[i], 1e-10);
}
}
}
}
@@ -153,7 +153,9 @@ public class LineartransValidationTests
// Create shifted series
var shifted = new TSeries();
for (int i = 0; i < series.Count; i++)
{
shifted.Add(new TValue(series[i].Time, series[i].Value + offset), true);
}
// a * (x + offset) should equal a*x + a*offset
var scaledSum = Lineartrans.Calculate(shifted, a, 0.0);
@@ -264,4 +266,4 @@ public class LineartransValidationTests
Assert.Equal(expected, result[i].Value, 1e-5);
}
}
}
}
+20
View File
@@ -38,9 +38,14 @@ public sealed class Lineartrans : AbstractBase
public Lineartrans(double slope = 1.0, double intercept = 0.0)
{
if (!double.IsFinite(slope))
{
throw new ArgumentException("Slope must be a finite number", nameof(slope));
}
if (!double.IsFinite(intercept))
{
throw new ArgumentException("Intercept must be a finite number", nameof(intercept));
}
_slope = slope;
_intercept = intercept;
@@ -67,9 +72,13 @@ public sealed class Lineartrans : AbstractBase
public override TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
_p_state = _state;
}
else
{
_state = _p_state;
}
double value = input.Value;
double result;
@@ -129,13 +138,24 @@ public sealed class Lineartrans : AbstractBase
double slope = 1.0, double intercept = 0.0)
{
if (source.Length == 0)
{
throw new ArgumentException("Source cannot be empty", nameof(source));
}
if (output.Length < source.Length)
{
throw new ArgumentException("Output length must be >= source length", nameof(output));
}
if (!double.IsFinite(slope))
{
throw new ArgumentException("Slope must be a finite number", nameof(slope));
}
if (!double.IsFinite(intercept))
{
throw new ArgumentException("Intercept must be a finite number", nameof(intercept));
}
// Check for non-finite values - if any exist, use scalar path only
// Note: For very large arrays, SIMD-based NaN detection could be faster,
@@ -117,4 +117,4 @@ public class LogtransIndicatorTests
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
}
}
}
}
+5 -2
View File
@@ -40,7 +40,10 @@ public class LogtransIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
if (_logtrans == null || _selector == null) return;
if (_logtrans == null || _selector == null)
{
return;
}
var item = HistoricalData[0, SeekOriginHistory.End];
double value = _selector(item);
@@ -53,4 +56,4 @@ public class LogtransIndicator : Indicator, IWatchlistIndicator
LinesSeries[0].SetValue(_logtrans.Last.Value, isHot, ShowColdValues);
}
}
}
+1 -1
View File
@@ -258,4 +258,4 @@ public class LogtransTests
// exp(logtrans(x)) should equal x
Assert.Equal(original, Math.Exp(logtransResult), Tolerance);
}
}
}
+9
View File
@@ -46,9 +46,13 @@ public sealed class Logtrans : AbstractBase
public override TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
_p_state = _state;
}
else
{
_state = _p_state;
}
// Handle non-positive and non-finite values
double value = input.Value;
@@ -108,9 +112,14 @@ public sealed class Logtrans : AbstractBase
public static void Calculate(ReadOnlySpan<double> source, Span<double> output)
{
if (source.Length == 0)
{
throw new ArgumentException("Source cannot be empty", nameof(source));
}
if (output.Length < source.Length)
{
throw new ArgumentException("Output length must be >= source length", nameof(output));
}
double lastValid = 0.0;
+4 -1
View File
@@ -43,7 +43,10 @@ public class LowestIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
if (_lowest == null || _selector == null) return;
if (_lowest == null || _selector == null)
{
return;
}
var item = HistoricalData[0, SeekOriginHistory.End];
double value = _selector(item);
+26
View File
@@ -33,7 +33,9 @@ public sealed class Lowest : AbstractBase
public Lowest(int period)
{
if (period < 1)
{
throw new ArgumentException("Period must be >= 1", nameof(period));
}
_period = period;
_buffer = new RingBuffer(period);
@@ -58,9 +60,13 @@ public sealed class Lowest : AbstractBase
public override TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
_p_state = _state;
}
else
{
_state = _p_state;
}
double value = double.IsFinite(input.Value) ? input.Value : _state.LastValid;
_state = new State(value);
@@ -112,11 +118,19 @@ public sealed class Lowest : AbstractBase
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period)
{
if (source.Length == 0)
{
throw new ArgumentException("Source cannot be empty", nameof(source));
}
if (output.Length < source.Length)
{
throw new ArgumentException("Output length must be >= source length", nameof(output));
}
if (period < 1)
{
throw new ArgumentException("Period must be >= 1", nameof(period));
}
int len = source.Length;
@@ -162,18 +176,25 @@ public sealed class Lowest : AbstractBase
// Remove indices outside window
while (dequeEnd > dequeStart && deque[dequeStart] <= i - period)
{
dequeStart++;
}
// Remove larger values from back (use values[] for corrected values)
while (dequeEnd > dequeStart && values[deque[dequeEnd - 1]] >= value)
{
dequeEnd--;
}
// Compact deque if needed
if (dequeEnd >= deque.Length)
{
int count = dequeEnd - dequeStart;
for (int j = 0; j < count; j++)
{
deque[j] = deque[dequeStart + j];
}
dequeStart = 0;
dequeEnd = count;
}
@@ -185,9 +206,14 @@ public sealed class Lowest : AbstractBase
finally
{
if (rentedDeque != null)
{
System.Buffers.ArrayPool<int>.Shared.Return(rentedDeque);
}
if (rentedValues != null)
{
System.Buffers.ArrayPool<double>.Shared.Return(rentedValues);
}
}
}
+4 -1
View File
@@ -43,7 +43,10 @@ public class MidpointIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
if (_midpoint == null || _selector == null) return;
if (_midpoint == null || _selector == null)
{
return;
}
var item = HistoricalData[0, SeekOriginHistory.End];
double value = _selector(item);
+15
View File
@@ -33,7 +33,9 @@ public sealed class Midpoint : AbstractBase
public Midpoint(int period)
{
if (period < 1)
{
throw new ArgumentException("Period must be >= 1", nameof(period));
}
_highest = new Highest(period);
_lowest = new Lowest(period);
@@ -116,11 +118,19 @@ public sealed class Midpoint : AbstractBase
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period)
{
if (source.Length == 0)
{
throw new ArgumentException("Source cannot be empty", nameof(source));
}
if (output.Length < source.Length)
{
throw new ArgumentException("Output length must be >= source length", nameof(output));
}
if (period < 1)
{
throw new ArgumentException("Period must be >= 1", nameof(period));
}
int len = source.Length;
@@ -151,9 +161,14 @@ public sealed class Midpoint : AbstractBase
finally
{
if (rentedHigh != null)
{
System.Buffers.ArrayPool<double>.Shared.Return(rentedHigh);
}
if (rentedLow != null)
{
System.Buffers.ArrayPool<double>.Shared.Return(rentedLow);
}
}
}
@@ -43,7 +43,10 @@ public class NormalizeIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
if (_normalize == null || _selector == null) return;
if (_normalize == null || _selector == null)
{
return;
}
var item = HistoricalData[0, SeekOriginHistory.End];
double value = _selector(item);
+7 -1
View File
@@ -152,7 +152,9 @@ public class NormalizeTests
var norm = new Normalize(5);
for (int i = 0; i < 10; i++)
{
norm.Update(new TValue(DateTime.UtcNow, i * 10));
}
Assert.True(norm.IsHot);
@@ -198,7 +200,9 @@ public class NormalizeTests
var series = _gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var tseries = new TSeries();
foreach (var bar in series)
{
tseries.Add(new TValue(bar.Time, bar.Close), true);
}
// Static calculation
var staticResult = Normalize.Calculate(tseries, 14);
@@ -207,7 +211,9 @@ public class NormalizeTests
var streamNorm = new Normalize(14);
var streamResult = new TSeries();
foreach (var bar in series)
{
streamResult.Add(streamNorm.Update(new TValue(bar.Time, bar.Close)), true);
}
// Compare last 50 values
for (int i = 50; i < 100; i++)
@@ -261,4 +267,4 @@ public class NormalizeTests
var result = norm.Update(new TValue(DateTime.UtcNow, 60));
Assert.Equal(0.2, result.Value, 1e-10);
}
}
}
@@ -40,7 +40,9 @@ public class NormalizeValidationTests
double[] values = { 10, 20, 30, 40, 50 };
foreach (var v in values)
{
norm.Update(new TValue(DateTime.UtcNow, v));
}
// Max value (50) should normalize to 1.0
Assert.Equal(1.0, norm.Last.Value, 1e-10);
@@ -55,7 +57,9 @@ public class NormalizeValidationTests
double[] values = { 50, 40, 30, 20, 10 };
foreach (var v in values)
{
norm.Update(new TValue(DateTime.UtcNow, v));
}
// Min value (10) should normalize to 0.0
Assert.Equal(0.0, norm.Last.Value, 1e-10);
@@ -98,7 +102,9 @@ public class NormalizeValidationTests
// All same values
for (int i = 0; i < 20; i++)
{
norm.Update(new TValue(DateTime.UtcNow, 42.0));
}
// Flat range: should return 0.5
Assert.Equal(0.5, norm.Last.Value, 1e-10);
@@ -223,7 +229,10 @@ public class NormalizeValidationTests
// Mode 2: Batch via Update(TSeries)
var tseries = new TSeries();
foreach (var bar in series)
{
tseries.Add(new TValue(bar.Time, bar.Close), true);
}
var results2 = Normalize.Calculate(tseries, period);
// Mode 3: Static span Calculate
@@ -235,7 +244,10 @@ public class NormalizeValidationTests
var source = new TSeries();
var norm4 = new Normalize(source, period);
foreach (var bar in series)
{
source.Add(new TValue(bar.Time, bar.Close), true);
}
var results4 = norm4.Last.Value;
// Compare all modes (use last 50 values for stability)
@@ -300,4 +312,4 @@ public class NormalizeValidationTests
Assert.True(norm.IsHot);
}
}
}
+34 -4
View File
@@ -37,7 +37,9 @@ public sealed class Normalize : AbstractBase
public Normalize(int period = 14)
{
if (period < 1)
{
throw new ArgumentException("Period must be >= 1", nameof(period));
}
_period = period;
_buffer = new RingBuffer(period);
@@ -64,7 +66,9 @@ public sealed class Normalize : AbstractBase
private static (double min, double max) FindMinMax(ReadOnlySpan<double> values)
{
if (values.Length == 0)
{
return (double.MaxValue, double.MinValue);
}
double min = values[0];
double max = values[0];
@@ -72,8 +76,15 @@ public sealed class Normalize : AbstractBase
for (int i = 1; i < values.Length; i++)
{
double v = values[i];
if (v < min) min = v;
if (v > max) max = v;
if (v < min)
{
min = v;
}
if (v > max)
{
max = v;
}
}
return (min, max);
@@ -83,9 +94,13 @@ public sealed class Normalize : AbstractBase
public override TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
_p_state = _state;
}
else
{
_state = _p_state;
}
double value = input.Value;
double result;
@@ -151,11 +166,19 @@ public sealed class Normalize : AbstractBase
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period = 14)
{
if (source.Length == 0)
{
throw new ArgumentException("Source cannot be empty", nameof(source));
}
if (output.Length < source.Length)
{
throw new ArgumentException("Output length must be >= source length", nameof(output));
}
if (period < 1)
{
throw new ArgumentException("Period must be >= 1", nameof(period));
}
double lastValid = 0.5;
@@ -181,8 +204,15 @@ public sealed class Normalize : AbstractBase
double v = source[j];
if (double.IsFinite(v))
{
if (v < min) min = v;
if (v > max) max = v;
if (v < min)
{
min = v;
}
if (v > max)
{
max = v;
}
}
}
+4 -1
View File
@@ -40,7 +40,10 @@ public class ReluIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
if (_relu == null || _selector == null) return;
if (_relu == null || _selector == null)
{
return;
}
var item = HistoricalData[0, SeekOriginHistory.End];
double value = _selector(item);
+14 -2
View File
@@ -61,9 +61,13 @@ public sealed class Relu : AbstractBase
public override TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
_p_state = _state;
}
else
{
_state = _p_state;
}
double value = input.Value;
double result;
@@ -85,7 +89,10 @@ public sealed class Relu : 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);
@@ -135,9 +142,14 @@ public sealed class Relu : AbstractBase
public static void Calculate(ReadOnlySpan<double> source, Span<double> output)
{
if (source.Length == 0)
{
throw new ArgumentException("Source cannot be empty", nameof(source));
}
if (output.Length < source.Length)
{
throw new ArgumentException("Output length must be >= source length", nameof(output));
}
double lastValid = 0.0;
int i = 0;
@@ -207,4 +219,4 @@ public sealed class Relu : AbstractBase
_p_state = default;
Last = default;
}
}
}
@@ -162,4 +162,4 @@ public class SigmoidIndicatorTests
now = now.AddMinutes(1);
}
}
}
}
+4 -1
View File
@@ -46,7 +46,10 @@ public class SigmoidIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
if (_sigmoid == null || _selector == null) return;
if (_sigmoid == null || _selector == null)
{
return;
}
var item = HistoricalData[0, SeekOriginHistory.End];
double value = _selector(item);
+13 -1
View File
@@ -219,7 +219,9 @@ public class SigmoidTests
var series = new TSeries();
for (int i = 0; i < 100; i++)
{
series.Add(new TValue(DateTime.UtcNow.AddSeconds(i), i - 50), isNew: true);
}
var result = sigmoid.Update(series);
@@ -232,7 +234,9 @@ public class SigmoidTests
var series = new TSeries();
for (int i = 0; i < 100; i++)
{
series.Add(new TValue(DateTime.UtcNow.AddSeconds(i), i - 50), isNew: true);
}
var result = Sigmoid.Calculate(series);
@@ -274,7 +278,9 @@ public class SigmoidTests
double[] source = new double[100];
var rng = new Random(42);
for (int i = 0; i < source.Length; i++)
{
source[i] = rng.NextDouble() * 200 - 100;
}
double[] spanOutput = new double[source.Length];
Sigmoid.Calculate(source.AsSpan(), spanOutput.AsSpan());
@@ -282,10 +288,14 @@ public class SigmoidTests
var sigmoid = new Sigmoid();
double[] streamOutput = new double[source.Length];
for (int i = 0; i < source.Length; i++)
{
streamOutput[i] = sigmoid.Update(new TValue(DateTime.UtcNow.AddSeconds(i), source[i]), true).Value;
}
for (int i = 0; i < source.Length; i++)
{
Assert.Equal(streamOutput[i], spanOutput[i], Epsilon);
}
}
[Fact]
@@ -315,7 +325,9 @@ public class SigmoidTests
sigmoid.Pub += (_, in _) => eventCount++;
for (int i = 0; i < 10; i++)
{
source.Add(new TValue(DateTime.UtcNow.AddSeconds(i), i), isNew: true);
}
Assert.Equal(10, eventCount);
}
@@ -351,4 +363,4 @@ public class SigmoidTests
Assert.Equal(0.5, result0.Value, Epsilon);
Assert.Equal(0.5, result100.Value, Epsilon);
}
}
}
@@ -222,7 +222,9 @@ public class SigmoidValidationTests
double[] source = new double[500];
var rng = new Random(42);
for (int i = 0; i < source.Length; i++)
{
source[i] = rng.NextDouble() * 200 - 50; // Range [-50, 150]
}
// Span calculation
double[] spanOutput = new double[source.Length];
@@ -236,4 +238,4 @@ public class SigmoidValidationTests
Assert.Equal(spanOutput[i], result.Value, Epsilon);
}
}
}
}
+24 -2
View File
@@ -38,7 +38,9 @@ public sealed class Sigmoid : AbstractBase
public Sigmoid(double k = 1.0, double x0 = 0.0)
{
if (k <= 0)
{
throw new ArgumentException("Steepness (k) must be positive", nameof(k));
}
_k = k;
_x0 = x0;
@@ -65,8 +67,16 @@ public sealed class Sigmoid : AbstractBase
{
double exponent = -k * (x - x0);
// Guard against overflow: exp(>709) overflows, exp(<-709) underflows to 0
if (exponent > 700) return 0.0; // exp(-700) ≈ 0
if (exponent < -700) return 1.0; // 1/(1+0) = 1
if (exponent > 700)
{
return 0.0; // exp(-700) ≈ 0
}
if (exponent < -700)
{
return 1.0; // 1/(1+0) = 1
}
return 1.0 / (1.0 + Math.Exp(exponent));
}
@@ -74,9 +84,13 @@ public sealed class Sigmoid : AbstractBase
public override TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
_p_state = _state;
}
else
{
_state = _p_state;
}
double value = input.Value;
double result;
@@ -134,11 +148,19 @@ public sealed class Sigmoid : AbstractBase
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, double k = 1.0, double x0 = 0.0)
{
if (source.Length == 0)
{
throw new ArgumentException("Source cannot be empty", nameof(source));
}
if (output.Length < source.Length)
{
throw new ArgumentException("Output length must be >= source length", nameof(output));
}
if (k <= 0)
{
throw new ArgumentException("Steepness (k) must be positive", nameof(k));
}
double lastValid = 0.5; // Sigmoid(x0) = 0.5
int i = 0;
+11 -1
View File
@@ -41,7 +41,10 @@ public class SlopeIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
if (_slope == null || _selector == null) return;
if (_slope == null || _selector == null)
{
return;
}
var item = HistoricalData[0, SeekOriginHistory.End];
double value = _selector(item);
@@ -60,11 +63,18 @@ public class SlopeIndicator : Indicator, IWatchlistIndicator
double slope = _slope.Last.Value;
Color color;
if (slope > 0)
{
color = Color.Green;
}
else if (slope < 0)
{
color = Color.Red;
}
else
{
color = Color.Gray;
}
LinesSeries[0].SetMarker(0, new IndicatorLineMarker(color));
}
}
+2 -2
View File
@@ -241,10 +241,10 @@ public class SlopeTests
{
var source = new TSeries();
var slope = new Slope(source);
source.Add(new TValue(DateTime.UtcNow, 10));
source.Add(new TValue(DateTime.UtcNow, 20));
Assert.True(slope.IsHot);
Assert.Equal(10, slope.Last.Value);
}
+14 -3
View File
@@ -98,7 +98,10 @@ public sealed class Slope : AbstractBase
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
int len = source.Count;
ReadOnlySpan<double> sourceValues = source.Values;
@@ -155,14 +158,22 @@ public sealed class Slope : 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));
}
int len = source.Length;
if (len == 0) return;
if (len == 0)
{
return;
}
// First element has no previous - set to 0
output[0] = 0.0;
if (len == 1) return;
if (len == 1)
{
return;
}
int i = 1;
@@ -137,4 +137,4 @@ public class SqrttransIndicatorTests
Assert.Equal(expectedRoots[i], indicator.LinesSeries[0].GetValue(0), 1e-10);
}
}
}
}
@@ -40,7 +40,10 @@ public class SqrttransIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
if (_sqrttrans == null || _selector == null) return;
if (_sqrttrans == null || _selector == null)
{
return;
}
var item = HistoricalData[0, SeekOriginHistory.End];
double value = _selector(item);
@@ -53,4 +56,4 @@ public class SqrttransIndicator : Indicator, IWatchlistIndicator
LinesSeries[0].SetValue(_sqrttrans.Last.Value, isHot, ShowColdValues);
}
}
}
+1 -1
View File
@@ -324,4 +324,4 @@ public class SqrttransTests
Assert.True(indicator.Last.Value >= 0);
}
}
}
}
@@ -236,4 +236,4 @@ public class SqrttransValidationTests
Assert.Equal(Math.Sqrt(largeValues[i]), indicator.Last.Value, Tolerance);
}
}
}
}
+9
View File
@@ -46,9 +46,13 @@ public sealed class Sqrttrans : AbstractBase
public override TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
_p_state = _state;
}
else
{
_state = _p_state;
}
double value = input.Value;
double result;
@@ -107,9 +111,14 @@ public sealed class Sqrttrans : AbstractBase
public static void Calculate(ReadOnlySpan<double> source, Span<double> output)
{
if (source.Length == 0)
{
throw new ArgumentException("Source cannot be empty", nameof(source));
}
if (output.Length < source.Length)
{
throw new ArgumentException("Output length must be >= source length", nameof(output));
}
double lastValid = 0.0; // sqrt(0) = 0