Refactor event handling and improve argument validation across indicators

- Updated event handler signatures to use TValueEventArgs for consistency in Mama, Mgdi, Pwma, Rma, Sma, Ssf, Super, T3, Tema, Trima, Usf, Vidya, Wma, and Atr classes.
- Enhanced argument validation by specifying parameter names in exceptions for clarity.
- Adjusted tests to align with new event handler signatures.
- Improved code readability and maintainability by using structured records and lambda expressions.
This commit is contained in:
Miha Kralj
2025-12-27 15:46:28 -08:00
parent 4750c2b1e8
commit d7dbd7078a
73 changed files with 502 additions and 300 deletions
+3
View File
@@ -9,3 +9,6 @@ echo "Modifying csproj files to target only net8.0 for Qodana..."
find . -name "*.csproj" -exec sed -i 's|<TargetFrameworks>net10.0;net8.0</TargetFrameworks>|<TargetFramework>net8.0</TargetFramework>|g' {} +
# Just in case some files still have single target net10.0
find . -name "*.csproj" -exec sed -i 's|<TargetFramework>net10.0</TargetFramework>|<TargetFramework>net8.0</TargetFramework>|g' {} +
echo "Restoring NuGet packages in container..."
dotnet restore --no-cache
+14 -2
View File
@@ -113,11 +113,16 @@
"git.decorations.enabled": true,
// ?????????????????????????????????????????????????????????????????
// Terminal Settings (Preserved from original)
// Terminal Settings - WSL Debian
// ?????????????????????????????????????????????????????????????????
"terminal.integrated.defaultProfile.windows": "PowerShell",
"terminal.integrated.defaultProfile.windows": "Debian",
"terminal.integrated.profiles.windows": {
"Debian": {
"path": "C:\\Windows\\System32\\wsl.exe",
"args": ["-d", "Debian", "--", "bash", "-l"],
"icon": "terminal-linux"
},
"PowerShell": {
"source": "PowerShell",
"icon": "terminal-powershell"
@@ -126,6 +131,13 @@
"terminal.integrated.shellIntegration.enabled": true,
"terminal.integrated.suggest.enabled": true,
// ?????????????????????????????????????????????????????????????????
// Cline Settings - WSL Debian Bash
// ?????????????????????????????????????????????????????????????????
"cline.terminalShell.windows": "C:\\Windows\\System32\\wsl.exe",
"cline.terminalShellArgs.windows": ["-d", "Debian", "--", "bash", "-l"],
// ?????????????????????????????????????????????????????????????????
// File Exclusions (Reduce Noise)
// ?????????????????????????????????????????????????????????????????
+7 -1
View File
@@ -22,6 +22,12 @@
<IsLocalBuild Condition="'$(GITHUB_ACTIONS)' == ''">true</IsLocalBuild>
</PropertyGroup>
<PropertyGroup>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<TreatWarningsAsErrors Condition="'$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>link</TrimMode>
@@ -45,7 +51,7 @@
</PropertyGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);S1144;S1944;S2053;S2222;S2245;S2259;S2583;S2589;S3329;S3655;S3776;S3900;S3949;S3966;S4158;S4347;S5773;S6781</NoWarn>
<NoWarn>$(NoWarn);S1144;S1944;S2053;S2222;S2245;S2259;S2583;S2589;S3329;S3655;S3776;S3900;S3949;S3966;S4158;S4347;S5773;S6781;MA0048;MA0051</NoWarn>
</PropertyGroup>
<ItemGroup>
+3 -3
View File
@@ -31,14 +31,14 @@ public abstract class AbstractBase : ITValuePublisher
/// <summary>
/// Event triggered when a new TValue is available.
/// </summary>
public event Action<TValue>? Pub;
public event TValuePublishedHandler? Pub;
/// <summary>
/// Helper to invoke the Pub event.
/// </summary>
protected void PubEvent(TValue value)
protected void PubEvent(TValue value, bool isNew = true)
{
Pub?.Invoke(value);
Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = isNew });
}
/// <summary>
+3 -3
View File
@@ -395,7 +395,7 @@ public static class SimdExtensions
public static void Add(ReadOnlySpan<double> left, ReadOnlySpan<double> right, Span<double> result)
{
if (left.Length != right.Length || left.Length != result.Length)
throw new ArgumentException("All spans must have the same length");
throw new ArgumentException("All spans must have the same length", nameof(result));
int i = 0;
if (Vector.IsHardwareAccelerated && left.Length >= Vector<double>.Count)
@@ -423,7 +423,7 @@ public static class SimdExtensions
public static void Subtract(ReadOnlySpan<double> left, ReadOnlySpan<double> right, Span<double> result)
{
if (left.Length != right.Length || left.Length != result.Length)
throw new ArgumentException("All spans must have the same length");
throw new ArgumentException("All spans must have the same length", nameof(result));
int i = 0;
if (Vector.IsHardwareAccelerated && left.Length >= Vector<double>.Count)
@@ -451,7 +451,7 @@ public static class SimdExtensions
public static double DotProduct(this ReadOnlySpan<double> a, ReadOnlySpan<double> b)
{
if (a.Length != b.Length)
throw new ArgumentException("Spans must have equal length");
throw new ArgumentException("Spans must have equal length", nameof(b));
if (a.IsEmpty) return 0.0;
+2
View File
@@ -1,4 +1,5 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
@@ -7,6 +8,7 @@ namespace QuanTAlib;
/// Pure data type: 48 bytes (long + 5 doubles).
/// </summary>
[SkipLocalsInit]
[StructLayout(LayoutKind.Auto)]
public readonly record struct TBar(long Time, double Open, double High, double Low, double Close, double Volume)
{
public DateTime AsDateTime => new(Time, DateTimeKind.Utc);
+4 -4
View File
@@ -358,8 +358,8 @@ namespace QuanTAlib.Tests
{
var series = new TBarSeries();
TBar? received = null;
series.Pub += bar => received = bar;
series.Pub += (object? sender, in TBarEventArgs args) => received = args.Value;
var barToAdd = new TBar(100, 10, 15, 5, 12, 100);
series.Add(barToAdd, isNew: true);
@@ -374,8 +374,8 @@ namespace QuanTAlib.Tests
var series = new TBarSeries();
TBar? received = null;
series.Add(100, 10, 15, 5, 12, 100);
series.Pub += bar => received = bar;
series.Pub += (object? sender, in TBarEventArgs args) => received = args.Value;
series.Add(100, 10, 18, 5, 15, 150, isNew: false);
Assert.NotNull(received);
+19 -3
View File
@@ -1,3 +1,4 @@
using System;
using System.Collections;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -9,17 +10,32 @@ namespace QuanTAlib;
/// Stores Time, Open, High, Low, Close, Volume in separate contiguous arrays for SIMD efficiency.
/// Exposes TSeries views for each component that share the underlying Time array.
/// </summary>
[StructLayout(LayoutKind.Auto)]
public readonly struct TBarEventArgs
{
public TBar Value { get; init; }
public bool IsNew { get; init; }
}
// Performance-focused event args struct; not derived from EventArgs by design.
// We intentionally deviate from the standard EventArgs pattern here for perf.
#pragma warning disable MA0046 // The second parameter must be of type 'System.EventArgs' or a derived type
public delegate void TBarPublishedHandler(object? sender, in TBarEventArgs args);
#pragma warning restore MA0046
public class TBarSeries : IReadOnlyList<TBar>
{
#pragma warning disable MA0016 // Prefer using collection abstraction instead of implementation
protected readonly List<long> _t;
protected readonly List<double> _o;
protected readonly List<double> _h;
protected readonly List<double> _l;
protected readonly List<double> _c;
protected readonly List<double> _v;
#pragma warning restore MA0016
public string Name { get; set; } = "Bar";
public event Action<TBar>? Pub;
public event TBarPublishedHandler? Pub;
// Note: These views share underlying storage. Do not modify directly; use TBarSeries.Add() instead.
public TSeries Open { get; }
@@ -102,7 +118,7 @@ public class TBarSeries : IReadOnlyList<TBar>
_v[lastIdx] = bar.Volume;
}
Pub?.Invoke(bar);
Pub?.Invoke(this, new TBarEventArgs { Value = bar, IsNew = isNew });
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -126,7 +142,7 @@ public class TBarSeries : IReadOnlyList<TBar>
hArr.Length != lArr.Length || lArr.Length != cArr.Length ||
cArr.Length != vArr.Length)
{
throw new ArgumentException("All arrays must have the same length");
throw new ArgumentException("All arrays must have the same length", nameof(t));
}
for (int i = 0; i < tArr.Length; i++)
+9 -1
View File
@@ -2,6 +2,14 @@ using System;
namespace QuanTAlib;
public sealed class TValueEventArgs : EventArgs
{
public TValue Value { get; init; }
public bool IsNew { get; init; }
}
public delegate void TValuePublishedHandler(object? sender, TValueEventArgs args);
/// <summary>
/// Interface for objects that publish TValue updates.
/// </summary>
@@ -10,5 +18,5 @@ public interface ITValuePublisher
/// <summary>
/// Event triggered when a new TValue is available.
/// </summary>
event Action<TValue> Pub;
event TValuePublishedHandler? Pub;
}
+2 -2
View File
@@ -298,7 +298,7 @@ public class TSeriesTests
{
var series = new TSeries();
TValue? received = null;
series.Pub += tv => received = tv;
series.Pub += (object? sender, TValueEventArgs args) => received = args.Value;
series.Add(100, 42.0);
@@ -313,7 +313,7 @@ public class TSeriesTests
var series = new TSeries();
TValue? received = null;
series.Add(100, 42.0);
series.Pub += tv => received = tv;
series.Pub += (object? sender, TValueEventArgs args) => received = args.Value;
series.Add(100, 43.0, isNew: false);
+17 -5
View File
@@ -13,12 +13,14 @@ namespace QuanTAlib;
/// </summary>
public class TSeries : IReadOnlyList<TValue>, ITValuePublisher
{
#pragma warning disable MA0016 // Prefer using collection abstraction instead of implementation
protected readonly List<long> _t;
protected readonly List<double> _v;
#pragma warning restore MA0016
public string Name { get; set; } = "Data";
public event Action<TValue>? Pub;
public event TValuePublishedHandler? Pub;
public TSeries() : this(0)
{
@@ -30,10 +32,18 @@ public class TSeries : IReadOnlyList<TValue>, ITValuePublisher
_v = new List<double>(capacity);
}
public TSeries(List<long> time, List<double> values)
public TSeries(IReadOnlyList<long> time, IReadOnlyList<double> values)
{
_t = time;
_v = values;
if (time is List<long> timeList && values is List<double> valueList)
{
_t = timeList;
_v = valueList;
}
else
{
_t = new List<long>(time);
_v = new List<double>(values);
}
}
public int Count
@@ -98,7 +108,9 @@ public class TSeries : IReadOnlyList<TValue>, ITValuePublisher
_t[lastIdx] = value.Time;
_v[lastIdx] = value.Value;
}
Pub?.Invoke(value);
var args = new TValueEventArgs { Value = value, IsNew = isNew };
Pub?.Invoke(this, args);
}
// Overload for backward compatibility (assumes isNew=true)
+2
View File
@@ -1,4 +1,5 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
@@ -7,6 +8,7 @@ namespace QuanTAlib;
/// Pure data type: 16 bytes (long + double).
/// </summary>
[SkipLocalsInit]
[StructLayout(LayoutKind.Auto)]
public readonly record struct TValue(long Time, double Value)
{
public DateTime AsDateTime => new(Time, DateTimeKind.Utc);
+4 -4
View File
@@ -13,7 +13,7 @@ public static class ValidationHelper
public const double TulipTolerance = 1e-7;
public const double RelativeTolerance = 0.005;
public static void VerifyData<TResult>(TSeries qSeries, List<TResult> sSeries, Func<TResult, double?> selector, int skip = 100, double tolerance = DefaultTolerance)
public static void VerifyData<TResult>(TSeries qSeries, IReadOnlyList<TResult> sSeries, Func<TResult, double?> selector, int skip = 100, double tolerance = DefaultTolerance)
{
Assert.Equal(qSeries.Count, sSeries.Count);
@@ -31,7 +31,7 @@ public static class ValidationHelper
}
}
public static void VerifyData<TResult>(List<double> qResults, List<TResult> sSeries, Func<TResult, double?> selector, int skip = 100, double tolerance = DefaultTolerance)
public static void VerifyData<TResult>(IReadOnlyList<double> qResults, IReadOnlyList<TResult> sSeries, Func<TResult, double?> selector, int skip = 100, double tolerance = DefaultTolerance)
{
Assert.Equal(qResults.Count, sSeries.Count);
@@ -87,7 +87,7 @@ public static class ValidationHelper
}
}
public static void VerifyData(List<double> qResults, double[] tOutput, int lookback, int skip = 100, double tolerance = DefaultTolerance)
public static void VerifyData(IReadOnlyList<double> qResults, double[] tOutput, int lookback, int skip = 100, double tolerance = DefaultTolerance)
{
int count = qResults.Count;
int start = Math.Max(0, count - skip);
@@ -148,7 +148,7 @@ public static class ValidationHelper
}
}
public static void VerifyData(List<double> qResults, double[] tOutput, Range outRange, int lookback, int skip = 100, double tolerance = DefaultTolerance)
public static void VerifyData(IReadOnlyList<double> qResults, double[] tOutput, Range outRange, int lookback, int skip = 100, double tolerance = DefaultTolerance)
{
int count = qResults.Count;
int start = Math.Max(0, count - skip);
+2 -2
View File
@@ -57,7 +57,7 @@ public sealed class Adx : ITValuePublisher
/// </summary>
public string Name { get; }
public event Action<TValue>? Pub;
public event TValuePublishedHandler? Pub;
/// <summary>
/// Current ADX value.
@@ -265,7 +265,7 @@ public sealed class Adx : ITValuePublisher
DiMinus = new TValue(input.Time, diMinus);
Last = new TValue(input.Time, _adx);
Pub?.Invoke(Last);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
return Last;
}
+3 -2
View File
@@ -30,7 +30,7 @@ public sealed class Adxr : ITValuePublisher
/// </summary>
public string Name { get; }
public event Action<TValue>? Pub;
public event TValuePublishedHandler? Pub;
/// <summary>
/// Current ADXR value.
@@ -130,7 +130,7 @@ public sealed class Adxr : ITValuePublisher
}
Last = new TValue(input.Time, adxr);
Pub?.Invoke(Last);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = true });
return Last;
}
@@ -233,3 +233,4 @@ public sealed class Adxr : ITValuePublisher
return new TSeries(tList, [.. v]);
}
}
+4 -4
View File
@@ -31,7 +31,7 @@ public sealed class Ao : ITValuePublisher
/// </summary>
public string Name { get; }
public event Action<TValue>? Pub;
public event TValuePublishedHandler? Pub;
/// <summary>
/// Current AO value.
@@ -99,7 +99,7 @@ public sealed class Ao : ITValuePublisher
double ao = sFast.Value - sSlow.Value;
Last = new TValue(input.Time, ao);
Pub?.Invoke(Last);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = true });
return Last;
}
@@ -117,7 +117,7 @@ public sealed class Ao : ITValuePublisher
double ao = sFast.Value - sSlow.Value;
Last = new TValue(input.Time, ao);
Pub?.Invoke(Last);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = true });
return Last;
}
@@ -167,7 +167,7 @@ public sealed class Ao : ITValuePublisher
public static void Calculate(ReadOnlySpan<double> high, ReadOnlySpan<double> low, Span<double> destination, int fastPeriod = 5, int slowPeriod = 34)
{
if (high.Length != low.Length || high.Length != destination.Length)
throw new ArgumentException("High, low, and destination spans must have the same length.");
throw new ArgumentException("High, low, and destination spans must have the same length.", nameof(destination));
int len = high.Length;
if (len == 0) return;
+11 -4
View File
@@ -26,13 +26,14 @@ public sealed class Apo : ITValuePublisher
{
private readonly Ema _emaFast;
private readonly Ema _emaSlow;
private readonly TValuePublishedHandler _handler;
/// <summary>
/// Display name for the indicator.
/// </summary>
public string Name { get; }
public event Action<TValue>? Pub;
public event TValuePublishedHandler? Pub;
/// <summary>
/// Current APO value.
@@ -65,6 +66,7 @@ public sealed class Apo : ITValuePublisher
_emaFast = new Ema(fastPeriod);
_emaSlow = new Ema(slowPeriod);
_handler = Handle;
WarmupPeriod = slowPeriod;
Name = $"Apo({fastPeriod},{slowPeriod})";
}
@@ -77,7 +79,7 @@ public sealed class Apo : ITValuePublisher
/// <param name="slowPeriod">Slow EMA period (default 26)</param>
public Apo(ITValuePublisher source, int fastPeriod = 12, int slowPeriod = 26) : this(fastPeriod, slowPeriod)
{
source.Pub += (item) => Update(item);
source.Pub += _handler;
}
/// <summary>
@@ -105,7 +107,7 @@ public sealed class Apo : ITValuePublisher
double apo = eFast.Value - eSlow.Value;
Last = new TValue(input.Time, apo);
Pub?.Invoke(Last);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
return Last;
}
@@ -143,6 +145,11 @@ public sealed class Apo : ITValuePublisher
return new TSeries(t, v);
}
private void Handle(object? sender, TValueEventArgs args)
{
Update(args.Value, args.IsNew);
}
/// <summary>
/// Calculates APO for the entire series using a new instance.
/// </summary>
@@ -167,7 +174,7 @@ public sealed class Apo : ITValuePublisher
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int fastPeriod = 12, int slowPeriod = 26)
{
if (source.Length != output.Length)
throw new ArgumentException("Source and output spans must be of the same length.");
throw new ArgumentException("Source and output spans must be of the same length.", nameof(output));
Span<double> fastEma = source.Length <= 1024 ? stackalloc double[source.Length] : new double[source.Length];
Span<double> slowEma = source.Length <= 1024 ? stackalloc double[source.Length] : new double[source.Length];
+3 -2
View File
@@ -32,7 +32,7 @@ public sealed class Aroon : ITValuePublisher
/// </summary>
public string Name { get; }
public event Action<TValue>? Pub;
public event TValuePublishedHandler? Pub;
/// <summary>
/// Current Aroon Oscillator value (Up - Down).
@@ -150,7 +150,7 @@ public sealed class Aroon : ITValuePublisher
Down = new TValue(input.Time, down);
Last = new TValue(input.Time, osc);
Pub?.Invoke(Last);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = true });
return Last;
}
@@ -255,3 +255,4 @@ public sealed class Aroon : ITValuePublisher
return new TSeries(tList, [.. v]);
}
}
+3 -2
View File
@@ -32,7 +32,7 @@ public sealed class AroonOsc : ITValuePublisher
/// </summary>
public string Name { get; }
public event Action<TValue>? Pub;
public event TValuePublishedHandler? Pub;
/// <summary>
/// Current Aroon Oscillator value.
@@ -136,7 +136,7 @@ public sealed class AroonOsc : ITValuePublisher
Last = new TValue(input.Time, osc);
Pub?.Invoke(Last);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = true });
return Last;
}
@@ -241,3 +241,4 @@ public sealed class AroonOsc : ITValuePublisher
return new TSeries(tList, [.. v]);
}
}
+4 -3
View File
@@ -32,7 +32,7 @@ public sealed class Bop : ITValuePublisher
/// </summary>
public static string Name => "Bop";
public event Action<TValue>? Pub;
public event TValuePublishedHandler? Pub;
/// <summary>
/// Current BOP value.
@@ -76,7 +76,7 @@ public sealed class Bop : ITValuePublisher
}
Last = new TValue(input.Time, bop);
Pub?.Invoke(Last);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = true });
return Last;
}
@@ -92,7 +92,7 @@ public sealed class Bop : ITValuePublisher
// Or we could throw NotSupportedException.
// Given the interface contract, returning 0 is safer than crashing.
Last = new TValue(input.Time, 0);
Pub?.Invoke(Last);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = true });
return Last;
}
@@ -180,3 +180,4 @@ public sealed class Bop : ITValuePublisher
return new TSeries(t, v);
}
}
+9 -3
View File
@@ -37,12 +37,14 @@ public sealed class Cfb : ITValuePublisher
private readonly double[] _runningSums;
private readonly double[] _p_runningSums;
[StructLayout(LayoutKind.Auto)]
private record struct State(double PrevCfb, double LastPrice, double LastValidValue);
private State _state;
private State _p_state;
private readonly TValuePublishedHandler _handler;
public string Name { get; }
public event Action<TValue>? Pub;
public event TValuePublishedHandler? Pub;
public TValue Last { get; private set; }
public bool IsHot => _prices.IsFull;
public int WarmupPeriod { get; }
@@ -81,14 +83,18 @@ public sealed class Cfb : ITValuePublisher
_p_runningSums = new double[_lengths.Length];
Name = "Jurik Composite Fractal Behavior";
_handler = Handle;
_state.PrevCfb = 1.0;
}
public Cfb(ITValuePublisher source, int[]? lengths = null) : this(lengths)
{
source.Pub += (item) => Update(item);
source.Pub += _handler;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Handle(object? sender, TValueEventArgs args) => Update(args.Value, args.IsNew);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset()
{
@@ -210,7 +216,7 @@ public sealed class Cfb : ITValuePublisher
_state.PrevCfb = cfb;
Last = new TValue(input.Time, cfb);
Pub?.Invoke(Last);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
return Last;
}
+3 -3
View File
@@ -23,7 +23,7 @@ public sealed class Dmx : ITValuePublisher
private bool _isInitialized;
public string Name { get; }
public event Action<TValue>? Pub;
public event TValuePublishedHandler? Pub;
public TValue Last { get; private set; }
public int WarmupPeriod { get; }
@@ -114,7 +114,7 @@ public sealed class Dmx : ITValuePublisher
double dmxValue = diPlus - diMinus;
Last = new TValue(input.Time, dmxValue);
Pub?.Invoke(Last);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = true });
return Last;
}
@@ -159,7 +159,7 @@ public sealed class Dmx : ITValuePublisher
return;
if (low.Length != len || close.Length != len || destination.Length != len)
throw new ArgumentException("All input spans must have the same length");
throw new ArgumentException("All input spans must have the same length", nameof(destination));
if (period <= 0)
throw new ArgumentException("Period must be greater than zero.", nameof(period));
+11 -4
View File
@@ -23,6 +23,7 @@ public sealed class Macd : ITValuePublisher
private readonly Ema _fastEma;
private readonly Ema _slowEma;
private readonly Ema _signalEma;
private readonly TValuePublishedHandler _handler;
public string Name { get; }
public bool IsHot => _fastEma.IsHot && _slowEma.IsHot && _signalEma.IsHot;
@@ -32,13 +33,14 @@ public sealed class Macd : ITValuePublisher
public TValue Signal { get; private set; }
public TValue Histogram { get; private set; }
public event Action<TValue>? Pub;
public event TValuePublishedHandler? Pub;
public Macd(int fastPeriod = 12, int slowPeriod = 26, int signalPeriod = 9)
{
_fastEma = new Ema(fastPeriod);
_slowEma = new Ema(slowPeriod);
_signalEma = new Ema(signalPeriod);
_handler = Handle;
Name = $"Macd({fastPeriod},{slowPeriod},{signalPeriod})";
WarmupPeriod = Math.Max(fastPeriod, slowPeriod) + signalPeriod;
@@ -47,7 +49,7 @@ public sealed class Macd : ITValuePublisher
public Macd(ITValuePublisher source, int fastPeriod = 12, int slowPeriod = 26, int signalPeriod = 9)
: this(fastPeriod, slowPeriod, signalPeriod)
{
source.Pub += (item) => Update(item);
source.Pub += _handler;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -78,7 +80,7 @@ public sealed class Macd : ITValuePublisher
Signal = signal;
Histogram = new TValue(input.Time, histValue);
Pub?.Invoke(Last);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
return Last;
}
@@ -100,6 +102,11 @@ public sealed class Macd : ITValuePublisher
return new TSeries(t, v);
}
private void Handle(object? sender, TValueEventArgs args)
{
Update(args.Value, args.IsNew);
}
/// <summary>
/// Calculates the MACD Line (Fast EMA - Slow EMA).
@@ -108,7 +115,7 @@ public sealed class Macd : ITValuePublisher
public static void Calculate(ReadOnlySpan<double> source, Span<double> destination, int fastPeriod = 12, int slowPeriod = 26)
{
if (source.Length != destination.Length)
throw new ArgumentException("Source and destination must be same length");
throw new ArgumentException("Source and destination must be same length", nameof(destination));
int len = source.Length;
double[] fastBuffer = ArrayPool<double>.Shared.Rent(len);
+10 -3
View File
@@ -26,6 +26,7 @@ public sealed class Rsi : AbstractBase
private readonly int _period;
private readonly Rma _avgGain;
private readonly Rma _avgLoss;
private readonly TValuePublishedHandler _handler;
private double _prevValue;
private double _p_prevValue;
@@ -39,6 +40,7 @@ public sealed class Rsi : AbstractBase
_period = period;
_avgGain = new Rma(period);
_avgLoss = new Rma(period);
_handler = Handle;
_prevValue = double.NaN;
_p_prevValue = double.NaN;
@@ -48,7 +50,7 @@ public sealed class Rsi : AbstractBase
public Rsi(ITValuePublisher source, int period = 14) : this(period)
{
source.Pub += (item) => Update(item);
source.Pub += _handler;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -110,7 +112,7 @@ public sealed class Rsi : AbstractBase
}
Last = new TValue(input.Time, rsi);
PubEvent(Last);
PubEvent(Last, isNew);
return Last;
}
@@ -145,6 +147,11 @@ public sealed class Rsi : AbstractBase
return new TSeries(t, v);
}
private void Handle(object? sender, TValueEventArgs args)
{
Update(args.Value, args.IsNew);
}
public override void Prime(ReadOnlySpan<double> source)
{
foreach (var value in source)
@@ -163,7 +170,7 @@ public sealed class Rsi : 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");
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));
+9 -4
View File
@@ -27,6 +27,7 @@ public sealed class Rsx : ITValuePublisher
private readonly int _period;
private readonly double _alpha;
[StructLayout(LayoutKind.Auto)]
private record struct State
{
// Momentum filters (3 stages, 2 filters each)
@@ -46,13 +47,14 @@ public sealed class Rsx : ITValuePublisher
private State _state;
private State _p_state;
private readonly TValuePublishedHandler _handler;
/// <summary>
/// Display name for the indicator.
/// </summary>
public string Name { get; }
public event Action<TValue>? Pub;
public event TValuePublishedHandler? Pub;
/// <summary>
/// The number of bars required to warm up the indicator.
@@ -72,11 +74,12 @@ public sealed class Rsx : ITValuePublisher
WarmupPeriod = period;
_alpha = 3.0 / (period + 2.0);
Name = $"Rsx({period})";
_handler = Handle;
}
public Rsx(ITValuePublisher source, int period) : this(period)
{
source.Pub += (item) => Update(item);
source.Pub += _handler;
}
/// <summary>
@@ -90,6 +93,8 @@ public sealed class Rsx : ITValuePublisher
public bool IsHot => _state.IsInitialized;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Handle(object? sender, TValueEventArgs args) => Update(args.Value, args.IsNew);
public TValue Update(TValue input, bool isNew = true)
{
if (isNew)
@@ -177,7 +182,7 @@ public sealed class Rsx : ITValuePublisher
}
Last = new TValue(input.Time, rsx);
Pub?.Invoke(Last);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
return Last;
}
@@ -219,7 +224,7 @@ public sealed class Rsx : ITValuePublisher
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");
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));
+9 -4
View File
@@ -22,12 +22,13 @@ public sealed class Vel : ITValuePublisher
private readonly Pwma _pwma;
private readonly Wma _wma;
private readonly int _period;
private readonly TValuePublishedHandler _handler;
public string Name { get; }
public TValue Last { get; private set; }
public bool IsHot => _pwma.IsHot && _wma.IsHot;
public int WarmupPeriod { get; }
public event Action<TValue>? Pub;
public event TValuePublishedHandler? Pub;
public Vel(int period)
{
@@ -38,13 +39,17 @@ public sealed class Vel : ITValuePublisher
_period = period;
WarmupPeriod = period;
Name = $"Vel({period})";
_handler = Handle;
}
public Vel(ITValuePublisher source, int period) : this(period)
{
source.Pub += (item) => Update(item);
source.Pub += _handler;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Handle(object? sender, TValueEventArgs args) => Update(args.Value, args.IsNew);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
{
@@ -52,7 +57,7 @@ public sealed class Vel : ITValuePublisher
var wma = _wma.Update(input, isNew);
Last = new TValue(input.Time, pwma.Value - wma.Value);
Pub?.Invoke(Last);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
return Last;
}
@@ -110,7 +115,7 @@ public sealed class Vel : ITValuePublisher
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");
throw new ArgumentException("Source and output must have the same length", nameof(output));
Span<double> pwma = source.Length <= 1024 ? stackalloc double[source.Length] : new double[source.Length];
Span<double> wma = source.Length <= 1024 ? stackalloc double[source.Length] : new double[source.Length];
+18
View File
@@ -43,11 +43,29 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="GitVersion.MsBuild" Version="6.5.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="JetBrains.Annotations" Version="2025.2.4" />
<PackageReference Include="Roslynator.Analyzers" Version="4.12.9">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.183">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.*">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
+2 -2
View File
@@ -180,7 +180,7 @@ public sealed class Covariance : AbstractBase
public static TSeries Calculate(TSeries sourceX, TSeries sourceY, int period, bool isPopulation = false)
{
if (sourceX.Count != sourceY.Count)
throw new ArgumentException("Source series must have the same length");
throw new ArgumentException("Source series must have the same length", nameof(sourceY));
int len = sourceX.Count;
var t = new List<long>(len);
@@ -201,7 +201,7 @@ public sealed class Covariance : AbstractBase
public static void Batch(ReadOnlySpan<double> sourceX, ReadOnlySpan<double> sourceY, Span<double> output, int period, bool isPopulation = false)
{
if (sourceX.Length != sourceY.Length || sourceX.Length != output.Length)
throw new ArgumentException("All spans must have the same length");
throw new ArgumentException("All spans must have the same length", nameof(output));
if (period < 2)
throw new ArgumentException("Period must be greater than or equal to 2", nameof(period));
+9 -3
View File
@@ -36,9 +36,11 @@ public sealed class LinReg : AbstractBase
private readonly double _sum_x;
private readonly double _denominator;
[StructLayout(LayoutKind.Auto)]
private record struct State(double SumY, double SumXY, double SumY2, double LastVal, double LastValidValue);
private State _state;
private State _p_state;
private readonly TValuePublishedHandler _handler;
private int _tickCount;
private const int ResyncInterval = 1000;
@@ -81,6 +83,7 @@ public sealed class LinReg : AbstractBase
_buffer = new RingBuffer(period);
Name = $"LinReg({period})";
WarmupPeriod = period;
_handler = Handle;
// Precalculate constants
// sum_x = 0 + 1 + ... + (n-1) = n(n-1)/2
@@ -95,9 +98,12 @@ public sealed class LinReg : AbstractBase
public LinReg(ITValuePublisher source, int period, int offset = 0) : this(period, offset)
{
source.Pub += (item) => Update(item);
source.Pub += _handler;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Handle(object? sender, TValueEventArgs args) => Update(args.Value, args.IsNew);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double GetValidValue(double input)
{
@@ -250,7 +256,7 @@ public sealed class LinReg : AbstractBase
}
Last = new TValue(input.Time, result);
PubEvent(Last);
PubEvent(Last, isNew);
return Last;
}
@@ -329,7 +335,7 @@ public sealed class LinReg : AbstractBase
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period, int offset = 0, double initialLastValid = 0)
{
if (source.Length != output.Length)
throw new ArgumentException("Source and output must have the same 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));
@@ -88,7 +88,7 @@ public sealed class MedianValidationTests : IDisposable
}
}
private static double CalculateMedian(List<double> sortedWindow)
private static double CalculateMedian(IReadOnlyList<double> sortedWindow)
{
int count = sortedWindow.Count;
if (count == 0) return 0; // Or NaN
+9 -4
View File
@@ -25,6 +25,7 @@ public sealed class Median : AbstractBase
private readonly int _period;
private readonly RingBuffer _buffer;
private readonly double[] _sortedBuffer;
private readonly TValuePublishedHandler _handler;
/// <summary>
/// Creates a Median indicator with the specified period.
@@ -40,11 +41,12 @@ public sealed class Median : AbstractBase
_sortedBuffer = new double[period];
Name = $"Median({period})";
WarmupPeriod = period;
_handler = Handle;
}
public Median(ITValuePublisher source, int period) : this(period)
{
source.Pub += (item) => Update(item);
source.Pub += _handler;
}
public Median(TSeries source, int period) : this(period)
@@ -54,7 +56,7 @@ public sealed class Median : AbstractBase
{
Last = new TValue(source.LastTime, Last.Value);
}
source.Pub += (item) => Update(item);
source.Pub += _handler;
}
/// <summary>
@@ -79,6 +81,9 @@ public sealed class Median : AbstractBase
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Handle(object? sender, TValueEventArgs args) => Update(args.Value, args.IsNew);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override TValue Update(TValue input, bool isNew = true)
{
@@ -123,7 +128,7 @@ public sealed class Median : AbstractBase
}
Last = new TValue(input.Time, median);
PubEvent(Last);
PubEvent(Last, isNew);
return Last;
}
@@ -202,7 +207,7 @@ public sealed class Median : 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");
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));
+1 -1
View File
@@ -205,7 +205,7 @@ public sealed class Skew : AbstractBase
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period, bool isPopulation = false)
{
if (source.Length != output.Length)
throw new ArgumentException("Source and output must have the same length");
throw new ArgumentException("Source and output must have the same length", nameof(output));
if (period < 3)
throw new ArgumentException("Period must be greater than or equal to 3", nameof(period));
+1 -1
View File
@@ -181,7 +181,7 @@ public sealed class Variance : AbstractBase
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period, bool isPopulation = false)
{
if (source.Length != output.Length)
throw new ArgumentException("Source and output must have the same length");
throw new ArgumentException("Source and output must have the same length", nameof(output));
if (period < 2)
throw new ArgumentException("Period must be greater than or equal to 2", nameof(period));
+6 -3
View File
@@ -33,7 +33,7 @@ public sealed class Alma : AbstractBase, IDisposable
private readonly double _invWeightSum;
private readonly RingBuffer _buffer;
private readonly ITValuePublisher? _source;
private readonly Action<TValue>? _pubHandler;
private readonly TValuePublishedHandler? _pubHandler;
private record struct State(double LastValidValue);
private State _state;
@@ -84,10 +84,13 @@ public sealed class Alma : AbstractBase, IDisposable
: this(period, offset, sigma)
{
_source = source;
_pubHandler = (item) => Update(item);
_pubHandler = Handle;
_source.Pub += _pubHandler;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Handle(object? sender, TValueEventArgs e) => Update(e.Value, e.IsNew);
public void Dispose()
{
if (_source != null && _pubHandler != null)
@@ -235,7 +238,7 @@ public sealed class Alma : AbstractBase, IDisposable
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");
throw new ArgumentException("Source and output must have the same length", nameof(output));
// Precompute weights
// Use stackalloc for small periods to avoid heap allocation, ArrayPool for large
+8 -4
View File
@@ -23,6 +23,7 @@ namespace QuanTAlib;
[SkipLocalsInit]
public sealed class Bessel : AbstractBase, IDisposable
{
[StructLayout(LayoutKind.Auto)]
private record struct State(double F1, double F2, double LastValidValue, int Count, bool IsHot)
{
public static State New() => new()
@@ -37,7 +38,7 @@ public sealed class Bessel : AbstractBase, IDisposable
private readonly double _c1, _c2, _c3;
private readonly ITValuePublisher? _publisher;
private readonly Action<TValue>? _handler;
private readonly TValuePublishedHandler? _handler;
private State _state = State.New();
private State _p_state = State.New();
@@ -68,7 +69,7 @@ public sealed class Bessel : AbstractBase, IDisposable
public Bessel(ITValuePublisher source, int length) : this(length)
{
_publisher = source;
_handler = item => Update(item);
_handler = Handle;
source.Pub += _handler;
}
@@ -84,7 +85,7 @@ public sealed class Bessel : AbstractBase, IDisposable
}
_publisher = source;
_handler = item => Update(item);
_handler = Handle;
source.Pub += _handler;
}
@@ -291,7 +292,7 @@ public sealed class Bessel : AbstractBase, IDisposable
throw new ArgumentException("Length must be greater than 0", nameof(length));
if (source.Length != output.Length)
throw new ArgumentException("Source and output must have the same length");
throw new ArgumentException("Source and output must have the same length", nameof(output));
if (source.Length == 0)
return;
@@ -309,6 +310,9 @@ public sealed class Bessel : AbstractBase, IDisposable
CalculateCore(source, output, c1, c2, c3, length, ref state);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Handle(object? sender, TValueEventArgs e) => Update(e.Value, e.IsNew);
public override void Reset()
{
_state = State.New();
@@ -111,7 +111,7 @@ public sealed class BilateralValidationTests : IDisposable
_output.WriteLine("Bilateral Span validated successfully against Reference");
}
private List<double> GetReferenceData(int period, double sigmaSRatio, double sigmaRMult)
private IReadOnlyList<double> GetReferenceData(int period, double sigmaSRatio, double sigmaRMult)
{
var reference = new BilateralReference(period, sigmaSRatio, sigmaRMult);
var results = new List<double>();
@@ -180,7 +180,7 @@ public sealed class BilateralValidationTests : IDisposable
return sumWeights < 1e-10 ? centerVal : sumWeightedSrc / sumWeights;
}
private static double CalculateStDev(List<double> values)
private static double CalculateStDev(IReadOnlyList<double> values)
{
if (values.Count < 2) return 0;
+8 -2
View File
@@ -28,9 +28,11 @@ public sealed class Bilateral : AbstractBase
private readonly RingBuffer _buffer;
private readonly double[] _spatialWeights;
[StructLayout(LayoutKind.Auto)]
private record struct State(double SumSq, double LastValidValue);
private State _state;
private State _p_state;
private readonly TValuePublishedHandler _handler;
/// <summary>
/// Creates a Bilateral Filter with specified parameters.
@@ -49,6 +51,7 @@ public sealed class Bilateral : AbstractBase
_buffer = new RingBuffer(period);
Name = $"Bilateral({period}, {sigmaSRatio:F2}, {sigmaRMult:F2})";
WarmupPeriod = period;
_handler = Handle;
_spatialWeights = new double[period];
PrecalculateSpatialWeights();
@@ -57,7 +60,7 @@ public sealed class Bilateral : AbstractBase
public Bilateral(ITValuePublisher source, int period, double sigmaSRatio = 0.5, double sigmaRMult = 1.0)
: this(period, sigmaSRatio, sigmaRMult)
{
source.Pub += (item) => Update(item);
source.Pub += _handler;
}
public override bool IsHot => _buffer.IsFull;
@@ -113,6 +116,9 @@ public sealed class Bilateral : AbstractBase
_p_state = _state;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Handle(object? sender, TValueEventArgs args) => Update(args.Value, args.IsNew);
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
@@ -171,7 +177,7 @@ public sealed class Bilateral : AbstractBase
double result = CalculateBilateral();
Last = new TValue(input.Time, result);
PubEvent(Last);
PubEvent(Last, isNew);
return Last;
}
+4 -2
View File
@@ -139,7 +139,8 @@ public class BlmaTests
var input = new double[] { 1, 2, 3, 4, 5 };
var timestamps = new List<DateTime>();
blma.Pub += (item) => timestamps.Add(item.AsDateTime);
blma.Pub += (object? sender, TValueEventArgs args) => timestamps.Add(args.Value.AsDateTime);
blma.Prime(input);
@@ -164,7 +165,8 @@ public class BlmaTests
];
var timestamps = new List<DateTime>();
blma.Pub += (item) => timestamps.Add(item.AsDateTime);
blma.Pub += (object? sender, TValueEventArgs args) => timestamps.Add(args.Value.AsDateTime);
blma.Prime(input);
+1 -1
View File
@@ -42,7 +42,7 @@ public class BlmaValidationTests
}
}
private static List<double> CalculateReference(TSeries source, int period)
private static IReadOnlyList<double> CalculateReference(TSeries source, int period)
{
var result = new List<double>();
var buffer = new List<double>();
+7 -5
View File
@@ -11,6 +11,7 @@ public sealed class Blma : AbstractBase, IDisposable
private readonly RingBuffer _buffer;
private readonly double[] _weights;
private readonly double _weightSum;
private readonly TValuePublishedHandler _handler;
private ITValuePublisher? _publisher;
private bool _hasLast;
@@ -31,26 +32,27 @@ public sealed class Blma : AbstractBase, IDisposable
// Pre-calculate weights for the full period
_weightSum = CalculateWeights(period, _weights);
_handler = Handle;
}
public Blma(ITValuePublisher source, int period) : this(period)
{
_publisher = source;
source.Pub += Handle;
source.Pub += _handler;
}
public void Dispose()
{
if (_publisher != null)
{
_publisher.Pub -= Handle;
_publisher.Pub -= _handler;
_publisher = null;
}
}
private void Handle(TValue value)
private void Handle(object? sender, TValueEventArgs args)
{
Update(value);
Update(args.Value, args.IsNew);
}
public override void Reset()
@@ -118,7 +120,7 @@ public sealed class Blma : AbstractBase, IDisposable
var tValue = new TValue(input.Time, result);
Last = tValue;
_hasLast = true;
PubEvent(tValue);
PubEvent(tValue, isNew);
return tValue;
}
+1 -1
View File
@@ -88,7 +88,7 @@ public class ButterValidationTests
}
}
private static List<double> CalculateReference(TSeries source, int period)
private static IReadOnlyList<double> CalculateReference(TSeries source, int period)
{
var result = new List<double>();
+9 -6
View File
@@ -1,5 +1,6 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
@@ -8,9 +9,11 @@ public sealed class Butter : AbstractBase
private readonly int _period;
private double _a1, _a2, _b0, _b1, _b2;
private double _invA0;
private readonly TValuePublishedHandler _handler;
private State _state;
private State _p_state;
[StructLayout(LayoutKind.Auto)]
private record struct State
{
public double X1, X2;
@@ -30,18 +33,18 @@ public sealed class Butter : AbstractBase
CalculateCoefficients();
Name = $"Butter({_period})";
WarmupPeriod = 2;
_handler = Handle;
Init();
}
public Butter(object source, int period) : this(period)
public Butter(ITValuePublisher source, int period) : this(period)
{
var pub = (ITValuePublisher)source;
pub.Pub += Handle;
source.Pub += _handler;
}
private void Handle(TValue value)
private void Handle(object? sender, TValueEventArgs args)
{
Update(value);
Update(args.Value, args.IsNew);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -121,7 +124,7 @@ public sealed class Butter : AbstractBase
var tValue = new TValue(input.Time, y);
Last = tValue;
PubEvent(tValue);
PubEvent(tValue, isNew);
return tValue;
}
+5 -3
View File
@@ -28,7 +28,7 @@ public sealed class Conv : AbstractBase, IDisposable
private readonly double[] _kernel;
private readonly RingBuffer _buffer;
private readonly ITValuePublisher? _source;
private readonly Action<TValue>? _subHandler;
private readonly TValuePublishedHandler? _subHandler;
private record struct State(double LastValidValue);
private State _state;
@@ -54,7 +54,7 @@ public sealed class Conv : AbstractBase, IDisposable
public Conv(ITValuePublisher source, double[] kernel) : this(kernel)
{
_source = source;
_subHandler = (item) => Update(item);
_subHandler = Handle;
_source.Pub += _subHandler;
}
@@ -66,6 +66,8 @@ public sealed class Conv : AbstractBase, IDisposable
}
}
private void Handle(object? sender, TValueEventArgs e) => Update(e.Value, e.IsNew);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double GetValidValue(double input)
{
@@ -204,7 +206,7 @@ public sealed class Conv : AbstractBase, IDisposable
public static void Batch(ReadOnlySpan<double> source, Span<double> output, double[] kernel)
{
if (source.Length != output.Length)
throw new ArgumentException("Source and output must have the same length");
throw new ArgumentException("Source and output must have the same length", nameof(output));
if (kernel == null || kernel.Length == 0)
throw new ArgumentException("Kernel must not be empty", nameof(kernel));
+6 -3
View File
@@ -24,6 +24,7 @@ namespace QuanTAlib;
[SkipLocalsInit]
public sealed class Dema : AbstractBase, IDisposable
{
[StructLayout(LayoutKind.Auto)]
private record struct EmaState(double Ema, double E, bool IsHot, bool IsCompensated)
{
public static EmaState New() => new() { Ema = 0, E = 1.0, IsHot = false, IsCompensated = false };
@@ -40,7 +41,7 @@ public sealed class Dema : AbstractBase, IDisposable
private double _lastValidValue = double.NaN;
private double _p_lastValidValue = double.NaN;
private readonly ITValuePublisher? _publisher;
private readonly Action<TValue>? _listener;
private readonly TValuePublishedHandler? _listener;
public override bool IsHot => _state2.IsHot;
@@ -57,7 +58,7 @@ public sealed class Dema : AbstractBase, IDisposable
public Dema(ITValuePublisher source, int period) : this(period)
{
_publisher = source;
_listener = (item) => Update(item);
_listener = Handle;
source.Pub += _listener;
}
@@ -230,7 +231,7 @@ public sealed class Dema : AbstractBase, IDisposable
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");
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));
@@ -328,4 +329,6 @@ public sealed class Dema : AbstractBase, IDisposable
_publisher.Pub -= _listener;
}
}
private void Handle(object? sender, TValueEventArgs e) => Update(e.Value, e.IsNew);
}
+10 -3
View File
@@ -20,6 +20,7 @@ public sealed class Dwma : AbstractBase
private readonly int _period;
private readonly Wma _wma1;
private readonly Wma _wma2;
private readonly TValuePublishedHandler _handler;
public override bool IsHot => _wma1.IsHot && _wma2.IsHot;
@@ -35,13 +36,14 @@ public sealed class Dwma : AbstractBase
_period = period;
_wma1 = new Wma(period);
_wma2 = new Wma(period);
_handler = Handle;
Name = $"Dwma({period})";
WarmupPeriod = period * 2;
}
public Dwma(ITValuePublisher source, int period) : this(period)
{
source.Pub += (item) => Update(item);
source.Pub += _handler;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -49,7 +51,7 @@ public sealed class Dwma : AbstractBase
{
TValue wma1Result = _wma1.Update(input, isNew);
Last = _wma2.Update(wma1Result, isNew);
PubEvent(Last);
PubEvent(Last, isNew);
return Last;
}
@@ -88,6 +90,11 @@ public sealed class Dwma : AbstractBase
return new TSeries(t, v);
}
private void Handle(object? sender, TValueEventArgs args)
{
Update(args.Value, args.IsNew);
}
public override void Prime(ReadOnlySpan<double> source)
{
Reset();
@@ -110,7 +117,7 @@ public sealed class Dwma : AbstractBase
throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than zero");
if (source.Length != output.Length)
throw new ArgumentException("Source and output must have the same length");
throw new ArgumentException("Source and output must have the same length", nameof(output));
// We need a temporary buffer for the first WMA pass
// Use stackalloc for small sizes, heap for large
+6 -2
View File
@@ -27,6 +27,7 @@ namespace QuanTAlib;
[SkipLocalsInit]
public sealed class Ema : AbstractBase
{
[StructLayout(LayoutKind.Auto)]
private record struct State(double Ema, double E, bool IsHot, bool IsCompensated)
{
public static State New() => new() { Ema = 0, E = 1.0, IsHot = false, IsCompensated = false };
@@ -63,7 +64,7 @@ public sealed class Ema : AbstractBase
/// <param name="period">Period for EMA calculation</param>
public Ema(ITValuePublisher source, int period) : this(period)
{
source.Pub += (item) => Update(item);
source.Pub += Handle;
}
public Ema(TSeries source, int period) : this(period)
@@ -73,7 +74,7 @@ public sealed class Ema : AbstractBase
{
Last = new TValue(source.LastTime, Last.Value);
}
source.Pub += (item) => Update(item);
source.Pub += Handle;
}
/// <summary>
@@ -180,6 +181,9 @@ public sealed class Ema : AbstractBase
_p_lastValidValue = _lastValidValue;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Handle(object? sender, TValueEventArgs e) => Update(e.Value, e.IsNew);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double GetValidValue(double input)
{
+10 -3
View File
@@ -27,6 +27,7 @@ public sealed class Hma : AbstractBase
private readonly Wma _wmaFull;
private readonly Wma _wmaHalf;
private readonly Wma _wmaSqrt;
private readonly TValuePublishedHandler _handler;
private int _sampleCount;
public override bool IsHot => _sampleCount >= WarmupPeriod;
@@ -42,6 +43,7 @@ public sealed class Hma : AbstractBase
_wmaFull = new Wma(period);
_wmaHalf = new Wma(halfPeriod);
_wmaSqrt = new Wma(_sqrtPeriod);
_handler = Handle;
Name = $"Hma({period})";
WarmupPeriod = period + _sqrtPeriod - 1; // WMA needs period, then WMA(sqrt) needs sqrt_period. Total lag/warmup.
@@ -49,7 +51,7 @@ public sealed class Hma : AbstractBase
public Hma(ITValuePublisher source, int period) : this(period)
{
source.Pub += (item) => Update(item);
source.Pub += _handler;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -69,7 +71,7 @@ public sealed class Hma : AbstractBase
// 4. Calculate HMA = WMA(sqrt(n), intermediate)
Last = _wmaSqrt.Update(new TValue(input.Time, intermediate), isNew);
PubEvent(Last);
PubEvent(Last, isNew);
return Last;
}
@@ -115,6 +117,11 @@ public sealed class Hma : AbstractBase
return new TSeries(t, v);
}
private void Handle(object? sender, TValueEventArgs args)
{
Update(args.Value, args.IsNew);
}
public override void Prime(ReadOnlySpan<double> source)
{
foreach (var value in source)
@@ -141,7 +148,7 @@ public sealed class Hma : 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");
throw new ArgumentException("Source and output must have the same length", nameof(output));
if (period <= 1)
throw new ArgumentException("Period must be greater than 1", nameof(period));
+12 -4
View File
@@ -20,6 +20,7 @@ public sealed class Htit : AbstractBase
{
public override bool IsHot => _state.Index >= WarmupPeriod;
[StructLayout(LayoutKind.Auto)]
private record struct State(
double I2, double Q2, double Re, double Im,
double Period, double SmoothPeriod,
@@ -34,6 +35,7 @@ public sealed class Htit : AbstractBase
private readonly RingBuffer _i1Buffer;
private readonly RingBuffer _q1Buffer;
private readonly RingBuffer _itBuffer;
private readonly TValuePublishedHandler _handler;
// High-precision constants
private const double c1 = 5.0 / 52.0; // ~0.09615385
@@ -47,7 +49,8 @@ public sealed class Htit : AbstractBase
{
Name = "Htit";
WarmupPeriod = 12;
_handler = Handle;
// Initialize buffers with size 8 (power of 2) for consistency with Calculate optimization
// except priceBuffer which needs to be larger for IT calculation
_priceBuffer = new RingBuffer(64); // Needs to hold enough history for IT calculation (up to 50 bars)
@@ -62,7 +65,7 @@ public sealed class Htit : AbstractBase
public Htit(ITValuePublisher source) : this()
{
source.Pub += (item) => Update(item);
source.Pub += _handler;
}
private void Init()
@@ -215,7 +218,7 @@ public sealed class Htit : AbstractBase
{
double val = Step(input.Value, isNew);
Last = new TValue(input.Time, val);
PubEvent(Last);
PubEvent(Last, isNew);
return Last;
}
@@ -237,6 +240,11 @@ public sealed class Htit : AbstractBase
return new TSeries(t, v);
}
private void Handle(object? sender, TValueEventArgs args)
{
Update(args.Value, args.IsNew);
}
public override void Prime(ReadOnlySpan<double> source)
{
foreach (var value in source)
@@ -255,7 +263,7 @@ 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");
throw new ArgumentException("Source and output must have the same length", nameof(output));
if (source.Length == 0) return;
+7 -2
View File
@@ -33,11 +33,13 @@ public sealed class Jma : AbstractBase
private readonly RingBuffer _devBuffer;
private readonly RingBuffer _volBuffer;
private readonly double[] _sorted;
private readonly TValuePublishedHandler _handler;
// Streaming state (current + previous snapshot for isNew=false)
private State _state;
private State _p_state;
[StructLayout(LayoutKind.Auto)]
private record struct State
{
// Jurik "envelope" anchors
@@ -97,6 +99,7 @@ public sealed class Jma : AbstractBase
// same warmup heuristic used in the AFL port (SetBarsRequired)
WarmupPeriod = (int)Math.Ceiling(20.0 + 80.0 * Math.Pow(period, 0.36));
_handler = Handle;
Name = $"Jma({period},{phase},{power})"; // power kept for signature compatibility
_devBuffer = new RingBuffer(DevWindowSize);
@@ -109,7 +112,7 @@ public sealed class Jma : AbstractBase
public Jma(ITValuePublisher source, int period, int phase = 0, double power = 0.45)
: this(period, phase, power)
{
source.Pub += item => Update(item);
source.Pub += _handler;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -238,7 +241,7 @@ public sealed class Jma : AbstractBase
{
double j = Step(input.Value, isNew);
Last = new TValue(input.Time, j);
PubEvent(Last);
PubEvent(Last, isNew);
return Last;
}
@@ -289,6 +292,8 @@ public sealed class Jma : AbstractBase
return new TSeries(t, v);
}
private void Handle(object? sender, TValueEventArgs args) => Update(args.Value, args.IsNew);
public override void Prime(ReadOnlySpan<double> source)
{
foreach (var value in source)
+11 -3
View File
@@ -25,7 +25,9 @@ public sealed class Kama : AbstractBase
private readonly double _fastAlpha;
private readonly double _slowAlpha;
private readonly RingBuffer _buffer;
private readonly TValuePublishedHandler _handler;
[StructLayout(LayoutKind.Auto)]
private record struct State(double Kama, double VolatilitySum, double NextDiffOut, double LastValidValue);
private State _state;
private State _p_state;
@@ -55,6 +57,7 @@ public sealed class Kama : AbstractBase
_fastAlpha = 2.0 / (fastPeriod + 1);
_slowAlpha = 2.0 / (slowPeriod + 1);
_handler = Handle;
Name = $"Kama({period}, {fastPeriod}, {slowPeriod})";
WarmupPeriod = period + 1;
@@ -68,7 +71,7 @@ public sealed class Kama : AbstractBase
public Kama(ITValuePublisher source, int period = 10, int fastPeriod = 2, int slowPeriod = 30)
: this(period, fastPeriod, slowPeriod)
{
source.Pub += (item) => Update(item);
source.Pub += _handler;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -177,7 +180,7 @@ public sealed class Kama : AbstractBase
}
Last = new TValue(input.Time, _state.Kama);
PubEvent(Last);
PubEvent(Last, isNew);
return Last;
}
@@ -205,6 +208,11 @@ public sealed class Kama : AbstractBase
return new TSeries(t, v);
}
private void Handle(object? sender, TValueEventArgs args)
{
Update(args.Value, args.IsNew);
}
public override void Prime(ReadOnlySpan<double> source)
{
foreach (var value in source)
@@ -225,7 +233,7 @@ public sealed class Kama : AbstractBase
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");
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);
+8 -3
View File
@@ -33,7 +33,9 @@ public sealed class Lsma : AbstractBase
private readonly double _sum_x;
private readonly double _denominator;
private readonly TValuePublishedHandler _handler;
[StructLayout(LayoutKind.Auto)]
private record struct State(double SumY, double SumXY, double LastVal, double LastValidValue);
private State _state;
private State _p_state;
@@ -59,6 +61,7 @@ public sealed class Lsma : AbstractBase
_buffer = new RingBuffer(period);
Name = $"Lsma({period})";
WarmupPeriod = period;
_handler = Handle;
// Precalculate constants
// sum_x = 0 + 1 + ... + (n-1) = n(n-1)/2
@@ -73,9 +76,11 @@ public sealed class Lsma : AbstractBase
public Lsma(ITValuePublisher source, int period, int offset = 0) : this(period, offset)
{
source.Pub += (item) => Update(item);
source.Pub += _handler;
}
private void Handle(object? sender, TValueEventArgs e) => Update(e.Value, e.IsNew);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double GetValidValue(double input)
{
@@ -197,7 +202,7 @@ public sealed class Lsma : AbstractBase
}
Last = new TValue(input.Time, result);
PubEvent(Last);
PubEvent(Last, isNew);
return Last;
}
@@ -282,7 +287,7 @@ public sealed class Lsma : AbstractBase
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period, int offset = 0, double initialLastValid = 0)
{
if (source.Length != output.Length)
throw new ArgumentException("Source and output must have the same 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));
+1 -1
View File
@@ -57,7 +57,7 @@ public class MamaTests
// Manually chain for test
bool eventFired = false;
mama.Pub += (item) => eventFired = true;
mama.Pub += (object? sender, TValueEventArgs args) => eventFired = true;
mama.Update(new TValue(DateTime.UtcNow, 100.0));
+8 -3
View File
@@ -18,7 +18,9 @@ public sealed class Mama : AbstractBase
private readonly double _fastLimit;
private readonly double _slowLimit;
private readonly double _scaledFastLimit;
private readonly TValuePublishedHandler _handler;
[StructLayout(LayoutKind.Auto)]
private record struct State(
double Period, double Phase, double Mama, double Fama, double SumPr,
double I2, double Q2, double Re, double Im, double LastValidPrice, int Index
@@ -55,7 +57,7 @@ public sealed class Mama : AbstractBase
{
if (fastLimit <= slowLimit || fastLimit <= 0 || slowLimit <= 0)
{
throw new ArgumentException("FastLimit must be > SlowLimit and > 0");
throw new ArgumentException("FastLimit must be > SlowLimit and > 0", nameof(fastLimit));
}
_fastLimit = fastLimit;
_slowLimit = slowLimit;
@@ -69,14 +71,17 @@ public sealed class Mama : AbstractBase
Name = $"Mama({fastLimit:F2},{slowLimit:F2})";
WarmupPeriod = 50;
_handler = Handle;
Init();
}
public Mama(ITValuePublisher source, double fastLimit = 0.5, double slowLimit = 0.05) : this(fastLimit, slowLimit)
{
source.Pub += (item) => Update(item);
source.Pub += _handler;
}
private void Handle(object? sender, TValueEventArgs e) => Update(e.Value, e.IsNew);
private void Init()
{
Reset();
@@ -229,7 +234,7 @@ public sealed class Mama : AbstractBase
double mama = Step(input.Value, isNew);
Last = new TValue(input.Time, mama);
Fama = new TValue(input.Time, _state.Fama);
PubEvent(Last);
PubEvent(Last, isNew);
return Last;
}
+8 -3
View File
@@ -23,7 +23,9 @@ public sealed class Mgdi : AbstractBase
{
private readonly int _period;
private readonly double _k;
private readonly TValuePublishedHandler _handler;
[StructLayout(LayoutKind.Auto)]
private record struct State(double LastMgdi, double LastValidValue, int Count, bool HasValidValue);
private State _state;
private State _p_state;
@@ -38,14 +40,17 @@ public sealed class Mgdi : AbstractBase
_k = k;
Name = $"Mgdi({period},{k})";
WarmupPeriod = period;
_handler = Handle;
Init();
}
public Mgdi(ITValuePublisher source, int period = 14, double k = 0.6) : this(period, k)
{
source.Pub += (item) => Update(item);
source.Pub += _handler;
}
private void Handle(object? sender, TValueEventArgs e) => Update(e.Value, e.IsNew);
private void Init()
{
_state = default;
@@ -110,7 +115,7 @@ public sealed class Mgdi : AbstractBase
}
Last = new TValue(input.Time, _state.LastMgdi);
PubEvent(Last);
PubEvent(Last, isNew);
return Last;
}
@@ -163,7 +168,7 @@ public sealed class Mgdi : AbstractBase
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");
throw new ArgumentException("Source and output must have the same length", nameof(output));
if (source.Length == 0) return;
+8 -3
View File
@@ -33,7 +33,9 @@ public sealed class Pwma : AbstractBase
private readonly double _divisor;
private readonly RingBuffer _buffer;
private readonly RingBuffer _p_buffer;
private readonly TValuePublishedHandler _handler;
[StructLayout(LayoutKind.Auto)]
private record struct State(double Sum, double WSum, double PSum, double LastInput, double LastValidValue, int TickCount);
private State _state;
private State _p_state;
@@ -52,13 +54,16 @@ public sealed class Pwma : AbstractBase
_p_buffer = new RingBuffer(period);
Name = $"Pwma({period})";
WarmupPeriod = period;
_handler = Handle;
}
public Pwma(ITValuePublisher source, int period) : this(period)
{
source.Pub += (item) => Update(item);
source.Pub += _handler;
}
private void Handle(object? sender, TValueEventArgs e) => Update(e.Value, e.IsNew);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double GetValidValue(double input)
{
@@ -152,7 +157,7 @@ public sealed class Pwma : AbstractBase
double count = _buffer.Count;
double currentDivisor = _buffer.IsFull ? _divisor : count * (count + 1.0) * (2.0 * count + 1.0) / 6.0;
Last = new TValue(input.Time, _state.PSum / currentDivisor);
PubEvent(Last);
PubEvent(Last, isNew);
return Last;
}
@@ -231,7 +236,7 @@ public sealed class Pwma : 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");
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));
+4 -2
View File
@@ -45,7 +45,7 @@ public sealed class Rma : AbstractBase
public Rma(ITValuePublisher source, int period) : this(period)
{
ArgumentNullException.ThrowIfNull(source);
source.Pub += (item) => Update(item);
source.Pub += Handle;
}
/// <summary>
@@ -61,7 +61,7 @@ public sealed class Rma : AbstractBase
{
Last = new TValue(source.LastTime, Last.Value);
}
source.Pub += (item) => Update(item);
source.Pub += Handle;
}
/// <summary>
@@ -80,6 +80,8 @@ public sealed class Rma : AbstractBase
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Handle(object? sender, TValueEventArgs e) => Update(e.Value, e.IsNew);
public override TValue Update(TValue input, bool isNew = true)
{
TValue result = _ema.Update(input, isNew);
+9 -4
View File
@@ -30,7 +30,9 @@ public sealed class Sma : AbstractBase
{
private readonly int _period;
private readonly RingBuffer _buffer;
private readonly TValuePublishedHandler _handler;
[StructLayout(LayoutKind.Auto)]
private record struct State(double Sum, double LastInput, double LastValidValue, int TickCount);
private State _state;
private State _p_state;
@@ -50,11 +52,12 @@ public sealed class Sma : AbstractBase
_buffer = new RingBuffer(period);
Name = $"Sma({period})";
WarmupPeriod = period;
_handler = Handle;
}
public Sma(ITValuePublisher source, int period) : this(period)
{
source.Pub += (item) => Update(item);
source.Pub += _handler;
}
public Sma(TSeries source, int period) : this(period)
@@ -64,9 +67,11 @@ public sealed class Sma : AbstractBase
{
Last = new TValue(source.LastTime, Last.Value);
}
source.Pub += (item) => Update(item);
source.Pub += _handler;
}
private void Handle(object? sender, TValueEventArgs e) => Update(e.Value, e.IsNew);
/////////////////////////////////////////////////////////////////////////////////////////////////
// Mode B: Streaming (Stateful)
/////////////////////////////////////////////////////////////////////////////////////////////////
@@ -198,7 +203,7 @@ public sealed class Sma : AbstractBase
double result = _buffer.Count > 0 ? _state.Sum / _buffer.Count : double.NaN;
Last = new TValue(input.Time, result);
PubEvent(Last);
PubEvent(Last, isNew);
return Last;
}
@@ -253,7 +258,7 @@ public sealed class Sma : 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");
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));
+9 -4
View File
@@ -19,12 +19,14 @@ namespace QuanTAlib;
[SkipLocalsInit]
public sealed class Ssf : AbstractBase
{
[StructLayout(LayoutKind.Auto)]
private record struct State(double Ssf1, double Ssf2, double PrevInput, double LastValidValue, int Count, bool IsHot)
{
public static State New() => new() { Ssf1 = 0, Ssf2 = 0, PrevInput = 0, LastValidValue = 0, Count = 0, IsHot = false };
}
private readonly double _c1, _c2, _c3;
private readonly TValuePublishedHandler _handler;
private State _state = State.New();
private State _p_state = State.New();
@@ -52,6 +54,7 @@ public sealed class Ssf : AbstractBase
Name = $"Ssf({period})";
WarmupPeriod = period;
_handler = Handle;
}
/// <summary>
@@ -61,7 +64,7 @@ public sealed class Ssf : AbstractBase
/// <param name="period">Period for SSF calculation</param>
public Ssf(ITValuePublisher source, int period) : this(period)
{
source.Pub += (item) => Update(item);
source.Pub += _handler;
}
public Ssf(TSeries source, int period) : this(period)
@@ -71,9 +74,11 @@ public sealed class Ssf : AbstractBase
{
Last = new TValue(source.LastTime, Last.Value);
}
source.Pub += (item) => Update(item);
source.Pub += _handler;
}
private void Handle(object? sender, TValueEventArgs e) => Update(e.Value, e.IsNew);
public override bool IsHot => _state.IsHot;
public override void Prime(ReadOnlySpan<double> source)
@@ -171,7 +176,7 @@ public sealed class Ssf : AbstractBase
_state.IsHot = true;
Last = new TValue(input.Time, ssf);
PubEvent(Last);
PubEvent(Last, isNew);
return Last;
}
@@ -276,7 +281,7 @@ public sealed class Ssf : AbstractBase
double c1 = 1.0 - c2 - c3;
if (source.Length != output.Length)
throw new ArgumentException("Source and output must have the same length");
throw new ArgumentException("Source and output must have the same length", nameof(output));
if (source.Length == 0) return;
+4 -2
View File
@@ -1,4 +1,5 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
@@ -15,6 +16,7 @@ public sealed class Super : ITValuePublisher
private TBar _lastInput;
private int _sampleCount;
[StructLayout(LayoutKind.Auto)]
private record struct State
{
public bool IsBullish;
@@ -33,7 +35,7 @@ public sealed class Super : ITValuePublisher
/// </summary>
public string Name => $"Super({_period},{_multiplier})";
public event Action<TValue>? Pub;
public event TValuePublishedHandler? Pub;
/// <summary>
/// Current SuperTrend value.
@@ -213,7 +215,7 @@ public sealed class Super : ITValuePublisher
UpperBand = new TValue(input.Time, upperBand);
LowerBand = new TValue(input.Time, lowerBand);
Pub?.Invoke(Last);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = true });
return Last;
}
+2 -1
View File
@@ -173,7 +173,7 @@ public class T3Tests
private class TestPublisher : ITValuePublisher
{
public event Action<TValue>? Pub;
public event TValuePublishedHandler? Pub;
public int SubscriberCount => Pub?.GetInvocationList().Length ?? 0;
}
@@ -222,3 +222,4 @@ public class T3Tests
Assert.Null(exception);
}
}
+8 -4
View File
@@ -27,11 +27,13 @@ namespace QuanTAlib;
[SkipLocalsInit]
public sealed class T3 : AbstractBase, IDisposable
{
[StructLayout(LayoutKind.Auto)]
private record struct State(double E1, double E2, double E3, double E4, double E5, double E6, bool IsInitialized)
{
public static State New() => new() { IsInitialized = false };
}
[StructLayout(LayoutKind.Auto)]
private readonly record struct Parameters(double Alpha, double C1, double C2, double C3, double C4);
private readonly Parameters _params;
@@ -40,7 +42,7 @@ public sealed class T3 : AbstractBase, IDisposable
private double _lastValidValue;
private double _p_lastValidValue;
private ITValuePublisher? _publisher;
private Action<TValue>? _handler;
private TValuePublishedHandler? _handler;
/// <summary>
/// Creates T3 with specified period and volume factor.
@@ -80,7 +82,7 @@ public sealed class T3 : AbstractBase, IDisposable
public T3(ITValuePublisher source, int period, double vfactor = 0.7) : this(period, vfactor)
{
_publisher = source;
_handler = (item) => Update(item);
_handler = Handle;
source.Pub += _handler;
}
@@ -98,10 +100,12 @@ public sealed class T3 : AbstractBase, IDisposable
{
Last = new TValue(source.LastTime, Last.Value);
}
_handler = (item) => Update(item);
_handler = Handle;
_publisher.Pub += _handler;
}
private void Handle(object? sender, TValueEventArgs e) => Update(e.Value, e.IsNew);
/// <summary>
/// True if the T3 has been initialized (received at least one value).
/// </summary>
@@ -285,7 +289,7 @@ public sealed class T3 : AbstractBase, IDisposable
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");
throw new ArgumentException("Source and output must have the same length", nameof(output));
double alpha = 2.0 / (period + 1);
double v = vfactor;
+10 -4
View File
@@ -26,6 +26,7 @@ namespace QuanTAlib;
[SkipLocalsInit]
public sealed class Tema : AbstractBase
{
[StructLayout(LayoutKind.Auto)]
private record struct EmaState(double Ema, double E, bool IsHot, bool IsCompensated)
{
public static EmaState New() => new() { Ema = 0, E = 1.0, IsHot = false, IsCompensated = false };
@@ -40,6 +41,7 @@ public sealed class Tema : AbstractBase
private EmaState _p_state1 = EmaState.New();
private EmaState _p_state2 = EmaState.New();
private EmaState _p_state3 = EmaState.New();
private readonly TValuePublishedHandler _handler;
private double _lastValidValue;
private double _p_lastValidValue;
@@ -54,11 +56,12 @@ public sealed class Tema : AbstractBase
_decay = 1.0 - _alpha;
Name = $"Tema({period})";
WarmupPeriod = period * 3;
_handler = Handle;
}
public Tema(ITValuePublisher source, int period) : this(period)
{
source.Pub += (item) => Update(item);
source.Pub += _handler;
}
public Tema(TSeries source, int period) : this(period)
@@ -68,7 +71,7 @@ public sealed class Tema : AbstractBase
{
Last = new TValue(source.LastTime, Last.Value);
}
source.Pub += (item) => Update(item);
source.Pub += _handler;
}
public Tema(double alpha)
@@ -79,8 +82,11 @@ public sealed class Tema : AbstractBase
_decay = 1.0 - alpha;
Name = $"Tema(α={alpha:F4})";
WarmupPeriod = (int)(3 * (2.0 / alpha - 1.0));
_handler = Handle;
}
private void Handle(object? sender, TValueEventArgs e) => Update(e.Value, e.IsNew);
/// <summary>
/// Initializes the indicator state using the provided history.
/// </summary>
@@ -203,7 +209,7 @@ public sealed class Tema : AbstractBase
double result = 3 * e1 - 3 * e2 + e3;
Last = new TValue(input.Time, result);
PubEvent(Last);
PubEvent(Last, isNew);
return Last;
}
@@ -315,7 +321,7 @@ 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");
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));
+8 -6
View File
@@ -30,7 +30,7 @@ public sealed class Trima : AbstractBase, IDisposable
private readonly int _period;
private readonly Sma _sma1;
private readonly Sma _sma2;
private readonly Action<TValue> _updateHandler;
private readonly TValuePublishedHandler _handler;
private ITValuePublisher? _publisher;
public Trima(int period)
@@ -43,7 +43,7 @@ public sealed class Trima : AbstractBase, IDisposable
_sma1 = new Sma(p1);
_sma2 = new Sma(p2);
_updateHandler = (item) => Update(item);
_handler = Handle;
Name = $"Trima({period})";
WarmupPeriod = p1 + p2 - 1;
@@ -52,14 +52,14 @@ public sealed class Trima : AbstractBase, IDisposable
public Trima(ITValuePublisher source, int period) : this(period)
{
_publisher = source;
source.Pub += _updateHandler;
source.Pub += _handler;
}
public void Dispose()
{
if (_publisher != null)
{
_publisher.Pub -= _updateHandler;
_publisher.Pub -= _handler;
_publisher = null;
}
}
@@ -73,7 +73,7 @@ public sealed class Trima : AbstractBase, IDisposable
TValue v2 = _sma2.Update(v1, isNew);
Last = v2;
PubEvent(Last);
PubEvent(Last, isNew);
return Last;
}
@@ -99,6 +99,8 @@ public sealed class Trima : AbstractBase, IDisposable
return new TSeries(t, v);
}
private void Handle(object? sender, TValueEventArgs args) => Update(args.Value, args.IsNew);
public override void Prime(ReadOnlySpan<double> source)
{
_sma1.Reset();
@@ -138,7 +140,7 @@ public sealed class Trima : AbstractBase, IDisposable
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");
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));
+9 -4
View File
@@ -20,12 +20,14 @@ namespace QuanTAlib;
[SkipLocalsInit]
public sealed class Usf : AbstractBase
{
[StructLayout(LayoutKind.Auto)]
private record struct State(double Usf1, double Usf2, double PrevInput1, double PrevInput2, double LastValidValue, int Count, bool IsHot)
{
public static State New() => new() { Usf1 = 0, Usf2 = 0, PrevInput1 = 0, PrevInput2 = 0, LastValidValue = double.NaN, Count = 0, IsHot = false };
}
private readonly double _c1, _c2, _c3;
private readonly TValuePublishedHandler _handler;
private State _state = State.New();
private State _p_state = State.New();
@@ -48,6 +50,7 @@ public sealed class Usf : AbstractBase
Name = $"Usf({period})";
WarmupPeriod = period;
_handler = Handle;
}
/// <summary>
@@ -57,7 +60,7 @@ public sealed class Usf : AbstractBase
/// <param name="period">Period for USF calculation</param>
public Usf(ITValuePublisher source, int period) : this(period)
{
source.Pub += (item) => Update(item);
source.Pub += _handler;
}
public Usf(TSeries source, int period) : this(period)
@@ -67,9 +70,11 @@ public sealed class Usf : AbstractBase
{
Last = new TValue(source.LastTime, Last.Value);
}
source.Pub += (item) => Update(item);
source.Pub += _handler;
}
private void Handle(object? sender, TValueEventArgs e) => Update(e.Value, e.IsNew);
public override bool IsHot => _state.IsHot;
public override void Prime(ReadOnlySpan<double> source)
@@ -171,7 +176,7 @@ public sealed class Usf : AbstractBase
_state.IsHot = true;
Last = new TValue(input.Time, usf);
PubEvent(Last);
PubEvent(Last, isNew);
return Last;
}
@@ -278,7 +283,7 @@ public sealed class Usf : AbstractBase
double c1 = (1.0 + c2 - c3) / 4.0;
if (source.Length != output.Length)
throw new ArgumentException("Source and output must have the same length");
throw new ArgumentException("Source and output must have the same length", nameof(output));
if (source.Length == 0) return;
+1 -1
View File
@@ -66,7 +66,7 @@ public class VidyaValidationTests
_output.WriteLine("VIDYA Batch validated successfully against reference implementation");
}
private static List<double> CalculateVidyaReference(TSeries data, int period)
private static IReadOnlyList<double> CalculateVidyaReference(TSeries data, int period)
{
var results = new List<double>();
var prices = data.Select(x => x.Value).ToList();
+6 -3
View File
@@ -33,8 +33,9 @@ public sealed class Vidya : AbstractBase, IDisposable
private readonly RingBuffer _ups;
private readonly RingBuffer _downs;
private readonly ITValuePublisher? _source;
private readonly Action<TValue>? _pubHandler;
private readonly TValuePublishedHandler? _pubHandler;
[StructLayout(LayoutKind.Auto)]
private record struct State(
double PrevClose, double LastVidya,
double CurrentClose, double CurrentVidya,
@@ -59,7 +60,7 @@ public sealed class Vidya : AbstractBase, IDisposable
public Vidya(ITValuePublisher source, int period) : this(period)
{
_source = source;
_pubHandler = (item) => Update(item);
_pubHandler = Handle;
source.Pub += _pubHandler;
}
@@ -71,6 +72,8 @@ public sealed class Vidya : AbstractBase, IDisposable
}
}
private void Handle(object? sender, TValueEventArgs e) => Update(e.Value, e.IsNew);
public override bool IsHot => _state.BarCount >= _period;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -277,7 +280,7 @@ public sealed class Vidya : AbstractBase, IDisposable
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");
throw new ArgumentException("Source and output must have the same length", nameof(output));
if (source.Length == 0) return;
+7 -4
View File
@@ -33,8 +33,9 @@ public sealed class Wma : AbstractBase, IDisposable
private readonly double _divisor;
private readonly RingBuffer _buffer;
private readonly ITValuePublisher? _source;
private readonly Action<TValue>? _handler;
private readonly TValuePublishedHandler? _handler;
[StructLayout(LayoutKind.Auto)]
private record struct State(double Sum, double WSum, double LastInput, double LastValidValue, int TickCount, bool HasSeenValidData);
private State _state;
private State _p_state;
@@ -68,7 +69,7 @@ public sealed class Wma : AbstractBase, IDisposable
public Wma(ITValuePublisher source, int period) : this(period)
{
_source = source;
_handler = (item) => Update(item);
_handler = Handle;
source.Pub += _handler;
}
@@ -159,7 +160,7 @@ public sealed class Wma : AbstractBase, IDisposable
double currentDivisor = _buffer.IsFull ? _divisor : (double)_buffer.Count * (_buffer.Count + 1) * 0.5;
Last = new TValue(input.Time, _state.WSum / currentDivisor);
PubEvent(Last);
PubEvent(Last, isNew);
return Last;
}
@@ -185,6 +186,8 @@ public sealed class Wma : AbstractBase, IDisposable
return new TSeries(t, v);
}
private void Handle(object? sender, TValueEventArgs e) => Update(e.Value, e.IsNew);
public override void Prime(ReadOnlySpan<double> source)
{
if (source.Length == 0) return;
@@ -248,7 +251,7 @@ public sealed class Wma : AbstractBase, IDisposable
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");
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));
+7 -3
View File
@@ -21,6 +21,7 @@ namespace QuanTAlib;
public sealed class Atr : AbstractBase
{
private readonly Rma _rma;
private readonly TValuePublishedHandler _handler;
private TBar _prevBar;
private bool _isInitialized;
@@ -37,6 +38,7 @@ public sealed class Atr : AbstractBase
Name = $"Atr({period})";
WarmupPeriod = period;
_isInitialized = false;
_handler = Handle;
}
/// <summary>
@@ -46,7 +48,7 @@ public sealed class Atr : AbstractBase
/// <param name="period">Period for ATR calculation</param>
public Atr(ITValuePublisher source, int period) : this(period)
{
source.Pub += (item) => Update(item);
source.Pub += _handler;
}
/// <summary>
@@ -62,6 +64,8 @@ public sealed class Atr : AbstractBase
// but we can rely on manual updates or the user subscribing.
}
private void Handle(object? sender, TValueEventArgs e) => Update(e.Value, e.IsNew);
/// <summary>
/// True if the ATR has warmed up and is providing valid results.
/// </summary>
@@ -119,7 +123,7 @@ public sealed class Atr : AbstractBase
TValue result = _rma.Update(new TValue(input.Time, tr), isNew);
Last = result;
PubEvent(Last);
PubEvent(Last, isNew);
return result;
}
@@ -132,7 +136,7 @@ public sealed class Atr : AbstractBase
// If user passes a single value, we assume it IS the True Range
TValue result = _rma.Update(input, isNew);
Last = result;
PubEvent(Last);
PubEvent(Last, isNew);
return result;
}
+2 -2
View File
@@ -103,8 +103,8 @@ public class AdlTests
{
var adl = new Adl();
bool eventFired = false;
adl.Pub += (val) => eventFired = true;
adl.Pub += (object? sender, TValueEventArgs args) => eventFired = true;
adl.Update(new TBar(DateTime.UtcNow, 10, 12, 8, 10, 100));
Assert.True(eventFired);
}
+5 -4
View File
@@ -33,7 +33,7 @@ public sealed class Adl : ITValuePublisher
/// </summary>
public static string Name => "ADL";
public event Action<TValue>? Pub;
public event TValuePublishedHandler? Pub;
/// <summary>
/// Current ADL value.
@@ -90,7 +90,7 @@ public sealed class Adl : ITValuePublisher
_isInitialized = true;
Last = new TValue(input.Time, _adl);
Pub?.Invoke(Last);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = true });
return Last;
}
@@ -106,7 +106,7 @@ public sealed class Adl : ITValuePublisher
}
Last = new TValue(input.Time, _adl);
Pub?.Invoke(Last);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = true });
return Last;
}
@@ -143,7 +143,7 @@ public sealed class Adl : ITValuePublisher
public static void Calculate(ReadOnlySpan<double> high, ReadOnlySpan<double> low, ReadOnlySpan<double> close, ReadOnlySpan<double> volume, Span<double> output)
{
if (high.Length != low.Length || high.Length != close.Length || high.Length != volume.Length || high.Length != output.Length)
throw new ArgumentException("All spans must be of the same length");
throw new ArgumentException("All spans must be of the same length", nameof(output));
int len = high.Length;
int i = 0;
@@ -197,3 +197,4 @@ public sealed class Adl : ITValuePublisher
}
}
}
+4 -3
View File
@@ -32,7 +32,7 @@ public sealed class Adosc : ITValuePublisher
/// </summary>
public string Name { get; }
public event Action<TValue>? Pub;
public event TValuePublishedHandler? Pub;
/// <summary>
/// Current ADOSC value.
@@ -96,7 +96,7 @@ public sealed class Adosc : ITValuePublisher
double adosc = eFast.Value - eSlow.Value;
Last = new TValue(input.Time, adosc);
Pub?.Invoke(Last);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = true });
return Last;
}
@@ -162,7 +162,7 @@ public sealed class Adosc : ITValuePublisher
public static void Calculate(ReadOnlySpan<double> high, ReadOnlySpan<double> low, ReadOnlySpan<double> close, ReadOnlySpan<double> volume, Span<double> output, int fastPeriod = 3, int slowPeriod = 10)
{
if (high.Length != output.Length)
throw new ArgumentException("Source and output spans must be of the same length.");
throw new ArgumentException("Source and output spans must be of the same length.", nameof(output));
Span<double> adl = high.Length <= 1024 ? stackalloc double[high.Length] : new double[high.Length];
Adl.Calculate(high, low, close, volume, adl);
@@ -176,3 +176,4 @@ public sealed class Adosc : ITValuePublisher
SimdExtensions.Subtract(fastEma, slowEma, output);
}
}
+49 -106
View File
@@ -1,115 +1,58 @@
#-------------------------------------------------------------------------------#
#-------------------------------------------------------------------------------#
# Qodana analysis is configured by qodana.yaml file #
# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
#-------------------------------------------------------------------------------#
version: "1.0"
#Specify inspection profile for code analysis
profile:
name: qodana.starter
name: qodana.recommended
#Enable inspections
#include:
# - name: <SomeEnabledInspectionId>
#Disable inspections
exclude:
# Disable XML documentation comment validation (allows unescaped < > in comments)
- name: XmlDocAnalyzer
- name: InvalidXmlDocComment
# Library project - public API properties are intentionally unused internally
- name: UnusedAutoPropertyAccessor.Global
# Style preference - fully qualified names used intentionally for clarity
- name: RedundantNameQualifier
- name: RCS1036 # Roslyn: Remove redundant empty line
- name: IDE0001 # Simplify name
- name: IDE0002 # Simplify member access
# HIGH-PERFORMANCE LIBRARY EXCLUSIONS
# ------------------------------------
# Flat namespace structure is intentional for this library
- name: CheckNamespace
# Float comparisons are intentional in financial calculations (checking 0.0, NaN, sentinel values)
- name: CompareOfFloatsByEqualityOperator
# Explicit default args improve code clarity and self-documentation
- name: RedundantArgumentDefaultValue
# Platform-specific optimizations (SIMD, intrinsics) are intentional
- name: CA1416 # Validate platform compatibility
# Public API unused internally - this is a library
- name: UnusedMember.Global
- name: MemberCanBePrivate.Global
- name: MemberCanBePrivate.Local
- name: ClassNeverInstantiated.Global
- name: UnusedType.Global
- name: UnusedMethodReturnValue.Global
- name: AutoPropertyCanBeMadeGetOnly.Global
- name: MemberCanBeMadeStatic.Global
- name: MemberCanBeMadeStatic.Local
# Redundant using directives - managed by IDE/build, not critical for library
- name: RedundantUsingDirective
- name: IDE0005 # Remove unnecessary using directives
- name: CS8019 # Unnecessary using directive
# Nullable warning suppressions - used intentionally for null safety patterns
- name: RedundantSuppressNullableWarningExpression
# Redundant type specifications - explicit types used for clarity/documentation
- name: RedundantTypeArgumentsOfMethod
- name: RedundantCast
- name: RedundantExplicitArrayCreation
# Unused local variables - often used in test setup or placeholder code (includes false positives for tuple deconstruction)
- name: UnusedVariable
- name: UnusedVariable.Compiler # False positive for tuple deconstruction
- name: CS0219 # Variable is assigned but never used
- name: RedundantAssignment # Value assigned is not used in any execution path
- name: UnusedAssignment # Assignment is not used
- name: IDE0059 # Unnecessary assignment of a value
# Object initializer in using statement - false positive for simple property setters
- name: CA2000 # Dispose objects before losing scope (overly cautious for simple cases)
- name: UseObjectOrCollectionInitializerWhenPossible
- name: DoNotUseObjectInitializerForUsingVariable
- name: ObjectInitializerMightCauseException
- name: ObjectCreationAsStatement
- name: UseObjectOrCollectionInitializer
- name: UsingStatementResourceInitialization # "Do not use object initializer for 'using' variable"
# Private field can be local variable - test fixtures often use fields for clarity/organization
- name: PrivateFieldCanBeConvertedToLocalVariable
- name: ConvertToLocalFunction
# Code coverage checks - SonarCloud handles coverage, Qodana coverage unreliable
- name: CoverageCheck
- name: ClassCoverageCheck
- name: MethodCoverageCheck
- name: CodeCoverageCheck
# Library-specific exclusions
- name: UnusedMember.Global # Suppresses "Method/Class is never used"
- name: UnusedMethodReturnValue.Global # Suppresses "Return value is never used"
- name: AutoPropertyCanBeMadeGetOnly.Global # Optional: common in libraries
#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
bootstrap: sh ./.github/prepare-qodana.sh
#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
#plugins:
# - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
#linter: jetbrains/qodana-dotnet:2025.3
linter: jetbrains/qodana-dotnet:2024.3
linter: jetbrains/qodana-dotnet:2025.3
dotnet:
solution: QuanTAlib.sln
coverage:
pathMappings:
- from: /home/runner/work/QuanTAlib/QuanTAlib
to: /data/project
frameworks: "net10.0"
configuration: Release
failureConditions:
severityThresholds:
critical: 0
high: 10
moderate: 50
low: 100
info: -1
exclude:
- name: All
paths:
- "**/bin/**"
- "**/obj/**"
- "**/packages/**"
- "**/TestResults/**"
- "**/.vs/**"
- "**/.vscode/**"
- "**/.idea/**"
- "**/.git/**"
- ".github/**"
- "**/.github/**"
- ".github/TradingPlatform.BusinessLayer.xml"
- "**/TradingPlatform.BusinessLayer.xml"
- "**/.agent/**"
- "**/.codacy/**"
- "**/.config/**"
- "**/.clinerules/**"
- "**/.idea/**"
- "**/.qodana/**"
- "**/.sonarlint/**"
- "**/coverage/**"
- "**/CoverageReport/**"
- "**/archive/**"
- "**/notebooks/**"
- "**/*.Designer.cs"
- "**/*.generated.cs"
- "**/results/**"
- "**/TestResults/**"
- "**/BenchmarkDotNet.Artifacts/**"
- name: InvalidXmlDocComment
- name: EnforceIfStatementBraces
- name: CheckNamespace
- name: LoopCanBeConvertedToQuery
+1 -1
View File
@@ -149,7 +149,7 @@ public class IndicatorExtensionsTests
var args = new PaintChartEventArgs(graphics, new Rectangle(0, 0, 100, 100));
// Test PaintSmoothCurve with different LineStyles and Warmup
foreach (LineStyle style in Enum.GetValues(typeof(LineStyle)))
foreach (LineStyle style in Enum.GetValues<LineStyle>())
{
var series = new LineSeries("Test", Color.Blue, 1, style);
for (int i = 0; i < 20; i++) series.AddValue();