mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-18 10:38:05 +00:00
code reviews
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using static QuanTAlib.IndicatorExtensions;
|
||||
|
||||
@@ -19,6 +20,11 @@ public class AccelIndicator : Indicator, IWatchlistIndicator
|
||||
private Accel? _accel;
|
||||
private Func<IHistoryItem, double>? _selector;
|
||||
|
||||
// Cached markers to avoid per-update allocations
|
||||
private static readonly IndicatorLineMarker GreenMarker = new(Color.Green);
|
||||
private static readonly IndicatorLineMarker RedMarker = new(Color.Red);
|
||||
private static readonly IndicatorLineMarker GrayMarker = new(Color.Gray);
|
||||
|
||||
public int MinHistoryDepths => 3;
|
||||
public override string ShortName => "ACCEL";
|
||||
|
||||
@@ -47,25 +53,42 @@ public class AccelIndicator : Indicator, IWatchlistIndicator
|
||||
double value = _selector(item);
|
||||
bool isNew = args.IsNewBar();
|
||||
|
||||
TValue input = new(item.TimeLeft, value);
|
||||
_accel.Update(input, isNew);
|
||||
ProcessUpdateCore(item.TimeLeft, value, isNew);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void ProcessUpdateCore(DateTime time, double value, bool isNew)
|
||||
{
|
||||
// Validate non-finite inputs - use last valid if not finite
|
||||
if (!double.IsFinite(value))
|
||||
{
|
||||
value = _accel!.Last.Value;
|
||||
if (!double.IsFinite(value))
|
||||
value = 0.0;
|
||||
}
|
||||
|
||||
TValue input = new(time, value);
|
||||
_accel!.Update(input, isNew);
|
||||
|
||||
bool isHot = _accel.IsHot;
|
||||
double accelValue = _accel.Last.Value; // Cache to avoid repeated property access
|
||||
|
||||
LinesSeries[0].SetValue(_accel.Last.Value, isHot, ShowColdValues);
|
||||
LinesSeries[0].SetValue(accelValue, isHot, ShowColdValues);
|
||||
LinesSeries[1].SetValue(0);
|
||||
|
||||
if (isHot || ShowColdValues)
|
||||
{
|
||||
double accel = _accel.Last.Value;
|
||||
Color color;
|
||||
if (accel > 0)
|
||||
color = Color.Green;
|
||||
else if (accel < 0)
|
||||
color = Color.Red;
|
||||
else
|
||||
color = Color.Gray;
|
||||
LinesSeries[0].SetMarker(0, new IndicatorLineMarker(color));
|
||||
// Use cached markers to avoid per-update allocations
|
||||
IndicatorLineMarker marker = GetMarker(accelValue);
|
||||
LinesSeries[0].SetMarker(0, marker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static IndicatorLineMarker GetMarker(double value)
|
||||
{
|
||||
if (value > 0) return GreenMarker;
|
||||
if (value < 0) return RedMarker;
|
||||
return GrayMarker;
|
||||
}
|
||||
}
|
||||
@@ -171,9 +171,14 @@ public sealed class Accel : AbstractBase
|
||||
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
foreach (double val in source)
|
||||
// TValue is a readonly record struct - no heap allocation occurs
|
||||
TimeSpan interval = step ?? TimeSpan.FromSeconds(1);
|
||||
DateTime time = DateTime.UtcNow - (interval * source.Length);
|
||||
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
Update(new TValue(DateTime.MinValue, val));
|
||||
Update(new TValue(time, source[i]), true);
|
||||
time += interval;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,4 +305,4 @@ public sealed class Accel : AbstractBase
|
||||
if (double.IsFinite(c)) return c;
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,11 @@ public class ChangeIndicator : Indicator, IWatchlistIndicator
|
||||
private Change? _change;
|
||||
private Func<IHistoryItem, double>? _selector;
|
||||
|
||||
// Cached markers to avoid per-update allocations
|
||||
private static readonly IndicatorLineMarker GreenMarker = new(Color.Green);
|
||||
private static readonly IndicatorLineMarker RedMarker = new(Color.Red);
|
||||
private static readonly IndicatorLineMarker GrayMarker = new(Color.Gray);
|
||||
|
||||
public int MinHistoryDepths => Period + 1;
|
||||
public override string ShortName => $"CHANGE({Period})";
|
||||
|
||||
@@ -55,21 +60,25 @@ public class ChangeIndicator : Indicator, IWatchlistIndicator
|
||||
_change.Update(input, isNew);
|
||||
|
||||
bool isHot = _change.IsHot;
|
||||
double changeValue = _change.Last.Value; // Cache to avoid repeated property access
|
||||
|
||||
LinesSeries[0].SetValue(_change.Last.Value, isHot, ShowColdValues);
|
||||
LinesSeries[0].SetValue(changeValue, isHot, ShowColdValues);
|
||||
LinesSeries[1].SetValue(0);
|
||||
|
||||
if (isHot || ShowColdValues)
|
||||
{
|
||||
double change = _change.Last.Value;
|
||||
Color color;
|
||||
if (change > 0)
|
||||
color = Color.Green;
|
||||
else if (change < 0)
|
||||
color = Color.Red;
|
||||
else
|
||||
color = Color.Gray;
|
||||
LinesSeries[0].SetMarker(0, new IndicatorLineMarker(color));
|
||||
// Use cached markers to avoid per-update allocations
|
||||
IndicatorLineMarker marker = GetMarker(changeValue);
|
||||
LinesSeries[0].SetMarker(0, marker);
|
||||
}
|
||||
}
|
||||
|
||||
[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;
|
||||
return GrayMarker;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,10 +162,16 @@ public class ChangeTests
|
||||
indicator.Update(_source[i]);
|
||||
}
|
||||
|
||||
// Compare last 10 values
|
||||
// Compare last 10 values between batch and streaming
|
||||
var streamResult = new TSeries();
|
||||
for (int j = 0; j < _source.Count; j++)
|
||||
{
|
||||
streamResult.Add(indicator.Update(_source[j]), true);
|
||||
}
|
||||
|
||||
for (int i = Math.Max(0, _source.Count - 10); i < _source.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult[i].Value, batchResult[i].Value, 1e-10);
|
||||
Assert.Equal(batchResult[i].Value, streamResult[i].Value, 1e-10);
|
||||
}
|
||||
|
||||
// Ensure final values match
|
||||
@@ -214,4 +220,4 @@ public class ChangeTests
|
||||
Assert.True(change.IsHot);
|
||||
Assert.NotEqual(0.0, change.Last.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,4 +116,76 @@ public class ExptransIndicatorTests
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExptransIndicator_NaNInput_ProducesFiniteOutput()
|
||||
{
|
||||
var indicator = new ExptransIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// First add a valid bar to establish last valid value
|
||||
indicator.HistoricalData.AddBar(now, 1, 2, 0, 1);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Add bar with NaN close - should use last valid value (1), so exp(1) = e
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), double.NaN, double.NaN, double.NaN, double.NaN);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
Assert.Equal(Math.E, indicator.LinesSeries[0].GetValue(0), 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExptransIndicator_InfinityInput_ProducesFiniteOutput()
|
||||
{
|
||||
var indicator = new ExptransIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// First add a valid bar to establish last valid value
|
||||
indicator.HistoricalData.AddBar(now, 1, 2, 0, 1);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Add bar with Infinity close - should use last valid value (1), so exp(1) = e
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), double.PositiveInfinity, double.PositiveInfinity, double.NegativeInfinity, double.PositiveInfinity);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
Assert.Equal(Math.E, indicator.LinesSeries[0].GetValue(0), 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExptransIndicator_NewTick_UpdatesSameBar()
|
||||
{
|
||||
var indicator = new ExptransIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 0, 1, -1, 0);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
double firstValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// NewTick should recalculate the same bar
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
|
||||
// Value should remain consistent (exp(0) = 1)
|
||||
Assert.Equal(1.0, indicator.LinesSeries[0].GetValue(0), 1e-10);
|
||||
Assert.Equal(firstValue, indicator.LinesSeries[0].GetValue(0), 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExptransIndicator_KnownValues_ComputesCorrectly()
|
||||
{
|
||||
var indicator = new ExptransIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// exp(2) ≈ 7.389
|
||||
indicator.HistoricalData.AddBar(now, 2, 3, 1, 2);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.Equal(Math.Exp(2), indicator.LinesSeries[0].GetValue(0), 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
// Transforms values using the exponential function e^x
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Numerics;
|
||||
using System.Runtime.Intrinsics;
|
||||
using System.Runtime.Intrinsics.X86;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
|
||||
@@ -129,7 +129,8 @@ public class HighestTests
|
||||
|
||||
indicator.Update(new TValue(time, 15.0));
|
||||
indicator.Update(new TValue(time.AddMinutes(1), double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(indicator.Last.Value));
|
||||
// Should use last valid value (15.0) instead of infinity
|
||||
Assert.Equal(15.0, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -296,4 +297,4 @@ public class HighestTests
|
||||
indicator.Update(new TValue(time.AddMinutes(5), 5.0));
|
||||
Assert.Equal(9.0, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,33 +148,48 @@ public sealed class Highest : AbstractBase
|
||||
}
|
||||
|
||||
// Second pass: compute rolling max using corrected values
|
||||
int dequeStart = 0;
|
||||
int dequeEnd = 0;
|
||||
// Use circular buffer indexing to avoid compaction overhead
|
||||
// Branch-based wrapping is faster than modulo in hot paths
|
||||
int head = 0; // front of deque (oldest/max)
|
||||
int tail = 0; // back of deque (newest)
|
||||
int count = 0; // number of elements in deque
|
||||
int capacity = deque.Length;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double value = values[i];
|
||||
|
||||
// Remove indices outside window
|
||||
while (dequeEnd > dequeStart && deque[dequeStart] <= i - period)
|
||||
dequeStart++;
|
||||
|
||||
// Remove smaller values from back
|
||||
while (dequeEnd > dequeStart && values[deque[dequeEnd - 1]] <= value)
|
||||
dequeEnd--;
|
||||
|
||||
// Compact deque if needed
|
||||
if (dequeEnd >= deque.Length)
|
||||
// Remove indices outside window from front
|
||||
while (count > 0 && deque[head] <= i - period)
|
||||
{
|
||||
int count = dequeEnd - dequeStart;
|
||||
for (int j = 0; j < count; j++)
|
||||
deque[j] = deque[dequeStart + j];
|
||||
dequeStart = 0;
|
||||
dequeEnd = count;
|
||||
head++;
|
||||
if (head >= capacity) head -= capacity;
|
||||
count--;
|
||||
}
|
||||
|
||||
deque[dequeEnd++] = i;
|
||||
output[i] = values[deque[dequeStart]];
|
||||
// Remove smaller values from back
|
||||
while (count > 0)
|
||||
{
|
||||
int backIdx = tail - 1;
|
||||
if (backIdx < 0) backIdx += capacity;
|
||||
if (values[deque[backIdx]] <= value)
|
||||
{
|
||||
tail = backIdx;
|
||||
count--;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add current index at tail
|
||||
deque[tail] = i;
|
||||
tail++;
|
||||
if (tail >= capacity) tail -= capacity;
|
||||
count++;
|
||||
|
||||
output[i] = values[deque[head]];
|
||||
}
|
||||
}
|
||||
finally
|
||||
@@ -193,4 +208,4 @@ public sealed class Highest : AbstractBase
|
||||
_p_state = default;
|
||||
Last = default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +184,8 @@ public class JerkIndicatorTests
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Cubic trend: changing acceleration = non-zero jerk
|
||||
// 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
|
||||
@@ -193,7 +194,8 @@ public class JerkIndicatorTests
|
||||
}
|
||||
|
||||
double lastJerk = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(lastJerk != 0);
|
||||
// For f(x) = x³, discrete third difference = 6
|
||||
Assert.Equal(6.0, lastJerk, 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -215,4 +217,4 @@ public class JerkIndicatorTests
|
||||
double lastJerk = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(0, lastJerk, 6);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class JerkTests
|
||||
@@ -302,4 +304,4 @@ public class JerkTests
|
||||
Assert.Equal(jerkResults[i], chainResults[i], precision: 9);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ public class LineartransIndicatorTests
|
||||
[Fact]
|
||||
public void LineartransIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new LineartransIndicator();
|
||||
var indicator = new LineartransIndicator { Slope = 2.0, Intercept = 5.0 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
@@ -90,6 +90,8 @@ public class LineartransIndicatorTests
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
// NewTick recalculates same bar: 2 * 100 + 5 = 205
|
||||
Assert.Equal(205.0, indicator.LinesSeries[0].GetValue(0), 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.Intrinsics;
|
||||
using System.Runtime.Intrinsics.X86;
|
||||
using System.Runtime.Intrinsics.Arm;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
@@ -119,6 +120,7 @@ public sealed class Lineartrans : AbstractBase
|
||||
|
||||
/// <summary>
|
||||
/// Calculates linear transformation over a span of values using SIMD when available.
|
||||
/// Uses FMA intrinsics for y = slope * x + intercept.
|
||||
/// </summary>
|
||||
public static void Calculate(ReadOnlySpan<double> source, Span<double> output,
|
||||
double slope = 1.0, double intercept = 0.0)
|
||||
@@ -132,34 +134,84 @@ public sealed class Lineartrans : AbstractBase
|
||||
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,
|
||||
// but for typical use cases the scalar pre-scan is sufficient
|
||||
bool hasNonFinite = false;
|
||||
for (int k = 0; k < source.Length && !hasNonFinite; k++)
|
||||
{
|
||||
hasNonFinite = !double.IsFinite(source[k]);
|
||||
}
|
||||
|
||||
double lastValid = 0.0;
|
||||
int i = 0;
|
||||
|
||||
// SIMD path for AVX2 (process 4 doubles at a time)
|
||||
if (Avx2.IsSupported && source.Length >= Vector256<double>.Count)
|
||||
// AVX512 FMA path (8 doubles at once)
|
||||
// Avx512F.FusedMultiplyAdd is independent of Fma.IsSupported
|
||||
if (!hasNonFinite && Avx512F.IsSupported && source.Length >= 8)
|
||||
{
|
||||
int vectorLength = source.Length - (source.Length % Vector256<double>.Count);
|
||||
var slopeVec = Vector512.Create(slope);
|
||||
var interceptVec = Vector512.Create(intercept);
|
||||
int simdEnd = source.Length - (source.Length % 8);
|
||||
|
||||
for (; i < vectorLength; i += Vector256<double>.Count)
|
||||
for (; i < simdEnd; i += 8)
|
||||
{
|
||||
// Check for finite values and handle last-valid
|
||||
for (int j = 0; j < Vector256<double>.Count; j++)
|
||||
{
|
||||
double val = source[i + j];
|
||||
if (double.IsFinite(val))
|
||||
{
|
||||
lastValid = Math.FusedMultiplyAdd(slope, val, intercept);
|
||||
output[i + j] = lastValid;
|
||||
}
|
||||
else
|
||||
{
|
||||
output[i + j] = lastValid;
|
||||
}
|
||||
}
|
||||
var vals = Vector512.Create(source.Slice(i, 8));
|
||||
var result = Avx512F.FusedMultiplyAdd(slopeVec, vals, interceptVec);
|
||||
result.CopyTo(output.Slice(i, 8));
|
||||
}
|
||||
lastValid = output[simdEnd - 1];
|
||||
}
|
||||
// AVX2 FMA path (4 doubles at once)
|
||||
else if (!hasNonFinite && Fma.IsSupported && source.Length >= 4)
|
||||
{
|
||||
var slopeVec = Vector256.Create(slope);
|
||||
var interceptVec = Vector256.Create(intercept);
|
||||
int simdEnd = source.Length - (source.Length % 4);
|
||||
|
||||
for (; i < simdEnd; i += 4)
|
||||
{
|
||||
var vals = Vector256.Create(source.Slice(i, 4));
|
||||
var result = Fma.MultiplyAdd(slopeVec, vals, interceptVec);
|
||||
result.CopyTo(output.Slice(i, 4));
|
||||
}
|
||||
lastValid = output[simdEnd - 1];
|
||||
}
|
||||
// SSE2 path (2 doubles at once) - fallback for x86/x64 without FMA
|
||||
// Note: This path uses Sse2.Multiply followed by Sse2.Add, which incurs two rounding
|
||||
// steps unlike the FMA paths above. Results may differ by ~1 ULP compared to FMA
|
||||
// on SSE2-only hardware (e.g., older x86/x64 CPUs without AVX2/FMA support).
|
||||
else if (!hasNonFinite && Sse2.IsSupported && source.Length >= 2)
|
||||
{
|
||||
var slopeVec = Vector128.Create(slope);
|
||||
var interceptVec = Vector128.Create(intercept);
|
||||
int simdEnd = source.Length - (source.Length % 2);
|
||||
|
||||
for (; i < simdEnd; i += 2)
|
||||
{
|
||||
var vals = Vector128.Create(source.Slice(i, 2));
|
||||
var result = Sse2.Add(Sse2.Multiply(slopeVec, vals), interceptVec);
|
||||
result.CopyTo(output.Slice(i, 2));
|
||||
}
|
||||
lastValid = output[simdEnd - 1];
|
||||
}
|
||||
// ARM64 NEON FMA path (2 doubles at once)
|
||||
else if (!hasNonFinite && AdvSimd.Arm64.IsSupported && source.Length >= 2)
|
||||
{
|
||||
var slopeVec = Vector128.Create(slope);
|
||||
var interceptVec = Vector128.Create(intercept);
|
||||
int simdEnd = source.Length - (source.Length % 2);
|
||||
|
||||
for (; i < simdEnd; i += 2)
|
||||
{
|
||||
var vals = Vector128.Create(source.Slice(i, 2));
|
||||
var result = AdvSimd.Arm64.FusedMultiplyAdd(interceptVec, vals, slopeVec);
|
||||
result.CopyTo(output.Slice(i, 2));
|
||||
}
|
||||
lastValid = output[simdEnd - 1];
|
||||
}
|
||||
|
||||
// Scalar fallback for remaining elements
|
||||
// Scalar fallback for remaining elements or when non-finite values exist
|
||||
for (; i < source.Length; i++)
|
||||
{
|
||||
double val = source[i];
|
||||
@@ -181,4 +233,4 @@ public sealed class Lineartrans : AbstractBase
|
||||
_p_state = default;
|
||||
Last = default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,27 +87,37 @@ public class LogtransValidationTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Logtrans_ProductRule()
|
||||
public void Logtrans_ZeroInput_UsesLastValid()
|
||||
{
|
||||
// ln(a*b) = ln(a) + ln(b)
|
||||
double a = 2.5;
|
||||
double b = 3.7;
|
||||
|
||||
// Zero input uses last valid value (robustness pattern)
|
||||
var indicator = new Logtrans();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, a));
|
||||
double lnA = indicator.Last.Value;
|
||||
// First update with valid value
|
||||
indicator.Update(new TValue(time, Math.E));
|
||||
double lastValid = indicator.Last.Value; // ln(e) = 1.0
|
||||
|
||||
indicator.Reset();
|
||||
indicator.Update(new TValue(time, b));
|
||||
double lnB = indicator.Last.Value;
|
||||
// Zero input - should use last valid
|
||||
indicator.Update(new TValue(time.AddMinutes(1), 0.0));
|
||||
|
||||
indicator.Reset();
|
||||
indicator.Update(new TValue(time, a * b));
|
||||
double lnAB = indicator.Last.Value;
|
||||
Assert.Equal(lastValid, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
Assert.Equal(lnA + lnB, lnAB, Tolerance);
|
||||
[Fact]
|
||||
public void Logtrans_NegativeInput_UsesLastValid()
|
||||
{
|
||||
// Negative input uses last valid value (robustness pattern)
|
||||
var indicator = new Logtrans();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// First update with valid value
|
||||
indicator.Update(new TValue(time, 2.0));
|
||||
double lastValid = indicator.Last.Value; // ln(2)
|
||||
|
||||
// Negative input - should use last valid
|
||||
indicator.Update(new TValue(time.AddMinutes(1), -1.0));
|
||||
|
||||
Assert.Equal(lastValid, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -153,4 +163,111 @@ public class LogtransValidationTests
|
||||
|
||||
Assert.Equal(n * lnA, lnAPowN, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Logtrans_VerySmallPositive_ApproachesNegativeInfinity()
|
||||
{
|
||||
// ln(ε) → -∞ as ε → 0+
|
||||
var indicator = new Logtrans();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, double.Epsilon));
|
||||
double result = indicator.Last.Value;
|
||||
|
||||
Assert.True(double.IsFinite(result));
|
||||
Assert.True(result < -700); // ln(double.Epsilon) ≈ -744
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Logtrans_VeryLargeValue_Handles()
|
||||
{
|
||||
// ln(large) should be finite
|
||||
var indicator = new Logtrans();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, 1e300));
|
||||
double result = indicator.Last.Value;
|
||||
|
||||
Assert.True(double.IsFinite(result));
|
||||
Assert.Equal(Math.Log(1e300), result, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Logtrans_Span_ZeroInput_UsesLastValid()
|
||||
{
|
||||
// Span API: zero input uses last valid value (robustness pattern)
|
||||
var values = new double[] { 2.0, 0.0, 3.0 };
|
||||
var output = new double[3];
|
||||
|
||||
Logtrans.Calculate(values, output);
|
||||
|
||||
Assert.Equal(Math.Log(2.0), output[0], Tolerance); // ln(2)
|
||||
Assert.Equal(Math.Log(2.0), output[1], Tolerance); // zero -> uses last valid (ln(2))
|
||||
Assert.Equal(Math.Log(3.0), output[2], Tolerance); // ln(3)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Logtrans_Span_NegativeInput_UsesLastValid()
|
||||
{
|
||||
// Span API: negative input uses last valid value (robustness pattern)
|
||||
var values = new double[] { 2.0, -5.0, 3.0 };
|
||||
var output = new double[3];
|
||||
|
||||
Logtrans.Calculate(values, output);
|
||||
|
||||
Assert.Equal(Math.Log(2.0), output[0], Tolerance); // ln(2)
|
||||
Assert.Equal(Math.Log(2.0), output[1], Tolerance); // negative -> uses last valid (ln(2))
|
||||
Assert.Equal(Math.Log(3.0), output[2], Tolerance); // ln(3)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Logtrans_NaNInput_UsesLastValid()
|
||||
{
|
||||
// NaN input uses last valid value (robustness pattern)
|
||||
var indicator = new Logtrans();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// First update with valid value
|
||||
indicator.Update(new TValue(time, Math.E));
|
||||
double lastValid = indicator.Last.Value; // ln(e) = 1.0
|
||||
|
||||
// NaN input - should use last valid
|
||||
indicator.Update(new TValue(time.AddMinutes(1), double.NaN));
|
||||
|
||||
Assert.Equal(lastValid, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Logtrans_PositiveInfinityInput_UsesLastValid()
|
||||
{
|
||||
// Positive infinity input uses last valid value (robustness pattern)
|
||||
var indicator = new Logtrans();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// First update with valid value
|
||||
indicator.Update(new TValue(time, 10.0));
|
||||
double lastValid = indicator.Last.Value; // ln(10)
|
||||
|
||||
// Positive infinity input - should use last valid
|
||||
indicator.Update(new TValue(time.AddMinutes(1), double.PositiveInfinity));
|
||||
|
||||
Assert.Equal(lastValid, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Logtrans_NegativeInfinityInput_UsesLastValid()
|
||||
{
|
||||
// Negative infinity input uses last valid value (robustness pattern)
|
||||
var indicator = new Logtrans();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// First update with valid value
|
||||
indicator.Update(new TValue(time, 5.0));
|
||||
double lastValid = indicator.Last.Value; // ln(5)
|
||||
|
||||
// Negative infinity input - should use last valid
|
||||
indicator.Update(new TValue(time.AddMinutes(1), double.NegativeInfinity));
|
||||
|
||||
Assert.Equal(lastValid, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
// Transforms values using natural logarithm (base e)
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Numerics;
|
||||
using System.Runtime.Intrinsics;
|
||||
using System.Runtime.Intrinsics.X86;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
@@ -102,7 +99,8 @@ public sealed class Logtrans : AbstractBase
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates natural logarithm over a span of values using SIMD when available.
|
||||
/// Calculates natural logarithm over a span of values.
|
||||
/// Note: Math.Log has no SIMD intrinsic; uses scalar path with last-valid substitution.
|
||||
/// </summary>
|
||||
public static void Calculate(ReadOnlySpan<double> source, Span<double> output)
|
||||
{
|
||||
@@ -112,34 +110,8 @@ public sealed class Logtrans : AbstractBase
|
||||
throw new ArgumentException("Output length must be >= source length", nameof(output));
|
||||
|
||||
double lastValid = 0.0;
|
||||
int i = 0;
|
||||
|
||||
// SIMD path for AVX2 (process 4 doubles at a time)
|
||||
if (Avx2.IsSupported && source.Length >= Vector256<double>.Count)
|
||||
{
|
||||
int vectorLength = source.Length - (source.Length % Vector256<double>.Count);
|
||||
|
||||
for (; i < vectorLength; i += Vector256<double>.Count)
|
||||
{
|
||||
// Process scalar for proper last-valid handling (Logtrans has no SIMD intrinsic)
|
||||
for (int j = 0; j < Vector256<double>.Count; j++)
|
||||
{
|
||||
double val = source[i + j];
|
||||
if (double.IsFinite(val) && val > 0)
|
||||
{
|
||||
lastValid = Math.Log(val);
|
||||
output[i + j] = lastValid;
|
||||
}
|
||||
else
|
||||
{
|
||||
output[i + j] = lastValid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scalar fallback for remaining elements
|
||||
for (; i < source.Length; i++)
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
double val = source[i];
|
||||
if (double.IsFinite(val) && val > 0)
|
||||
|
||||
Reference in New Issue
Block a user