From d7dbd7078a299ba22d2dd4f0328fa79ebe0766e1 Mon Sep 17 00:00:00 2001 From: Miha Kralj Date: Sat, 27 Dec 2025 15:46:28 -0800 Subject: [PATCH] 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. --- .github/prepare-qodana.sh | 3 + .vscode/settings.json | 16 +- Directory.Build.props | 8 +- lib/core/AbstractBase.cs | 6 +- lib/core/simd/SimdExtensions.cs | 6 +- lib/core/tbar/tbar.cs | 2 + lib/core/tbarseries/TBarSeries.Tests.cs | 8 +- lib/core/tbarseries/tbarseries.cs | 22 ++- lib/core/tseries/ITValuePublisher.cs | 10 +- lib/core/tseries/TSeries.Tests.cs | 4 +- lib/core/tseries/tseries.cs | 22 ++- lib/core/tvalue/tvalue.cs | 2 + lib/feeds/gbm/ValidationHelper.cs | 8 +- lib/momentum/adx/Adx.cs | 4 +- lib/momentum/adxr/Adxr.cs | 5 +- lib/momentum/ao/Ao.cs | 8 +- lib/momentum/apo/Apo.cs | 15 +- lib/momentum/aroon/Aroon.cs | 5 +- lib/momentum/aroonosc/AroonOsc.cs | 5 +- lib/momentum/bop/Bop.cs | 7 +- lib/momentum/cfb/Cfb.cs | 12 +- lib/momentum/dmx/Dmx.cs | 6 +- lib/momentum/macd/Macd.cs | 15 +- lib/momentum/rsi/Rsi.cs | 13 +- lib/momentum/rsx/Rsx.cs | 13 +- lib/momentum/vel/Vel.cs | 13 +- lib/quantalib.csproj | 18 ++ lib/statistics/covariance/Covariance.cs | 4 +- lib/statistics/linreg/LinReg.cs | 12 +- .../median/Median.Validation.Tests.cs | 2 +- lib/statistics/median/Median.cs | 13 +- lib/statistics/skew/Skew.cs | 2 +- lib/statistics/variance/Variance.cs | 2 +- lib/trends/alma/Alma.cs | 9 +- lib/trends/bessel/Bessel.cs | 12 +- .../bilateral/Bilateral.Validation.Tests.cs | 4 +- lib/trends/bilateral/Bilateral.cs | 10 +- lib/trends/blma/Blma.Tests.cs | 6 +- lib/trends/blma/Blma.Validation.Tests.cs | 2 +- lib/trends/blma/Blma.cs | 12 +- lib/trends/butter/Butter.Validation.Tests.cs | 2 +- lib/trends/butter/Butter.cs | 15 +- lib/trends/conv/Conv.cs | 8 +- lib/trends/dema/Dema.cs | 9 +- lib/trends/dwma/Dwma.cs | 13 +- lib/trends/ema/Ema.cs | 8 +- lib/trends/hma/Hma.cs | 13 +- lib/trends/htit/Htit.cs | 16 +- lib/trends/jma/Jma.cs | 9 +- lib/trends/kama/Kama.cs | 14 +- lib/trends/lsma/Lsma.cs | 11 +- lib/trends/mama/Mama.Tests.cs | 2 +- lib/trends/mama/Mama.cs | 11 +- lib/trends/mgdi/Mgdi.cs | 11 +- lib/trends/pwma/Pwma.cs | 11 +- lib/trends/rma/Rma.cs | 6 +- lib/trends/sma/Sma.cs | 13 +- lib/trends/ssf/Ssf.cs | 13 +- lib/trends/super/Super.cs | 6 +- lib/trends/t3/T3.Tests.cs | 3 +- lib/trends/t3/T3.cs | 12 +- lib/trends/tema/Tema.cs | 14 +- lib/trends/trima/Trima.cs | 14 +- lib/trends/usf/Usf.cs | 13 +- lib/trends/vidya/Vidya.Validation.Tests.cs | 2 +- lib/trends/vidya/Vidya.cs | 9 +- lib/trends/wma/Wma.cs | 11 +- lib/volatility/atr/Atr.cs | 10 +- lib/volume/adl/Adl.Tests.cs | 4 +- lib/volume/adl/Adl.cs | 9 +- lib/volume/adosc/Adosc.cs | 7 +- qodana.yaml | 155 ++++++------------ quantower/IndicatorExtensions.Tests.cs | 2 +- 73 files changed, 502 insertions(+), 300 deletions(-) diff --git a/.github/prepare-qodana.sh b/.github/prepare-qodana.sh index fad2884f..cf4f3961 100644 --- a/.github/prepare-qodana.sh +++ b/.github/prepare-qodana.sh @@ -9,3 +9,6 @@ echo "Modifying csproj files to target only net8.0 for Qodana..." find . -name "*.csproj" -exec sed -i 's|net10.0;net8.0|net8.0|g' {} + # Just in case some files still have single target net10.0 find . -name "*.csproj" -exec sed -i 's|net10.0|net8.0|g' {} + + +echo "Restoring NuGet packages in container..." +dotnet restore --no-cache diff --git a/.vscode/settings.json b/.vscode/settings.json index cefe716d..895dc982 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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) // ????????????????????????????????????????????????????????????????? diff --git a/Directory.Build.props b/Directory.Build.props index 3a250da2..72968333 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -22,6 +22,12 @@ true + + true + true + true + + true link @@ -45,7 +51,7 @@ - $(NoWarn);S1144;S1944;S2053;S2222;S2245;S2259;S2583;S2589;S3329;S3655;S3776;S3900;S3949;S3966;S4158;S4347;S5773;S6781 + $(NoWarn);S1144;S1944;S2053;S2222;S2245;S2259;S2583;S2589;S3329;S3655;S3776;S3900;S3949;S3966;S4158;S4347;S5773;S6781;MA0048;MA0051 diff --git a/lib/core/AbstractBase.cs b/lib/core/AbstractBase.cs index 7e67d639..0434ca33 100644 --- a/lib/core/AbstractBase.cs +++ b/lib/core/AbstractBase.cs @@ -31,14 +31,14 @@ public abstract class AbstractBase : ITValuePublisher /// /// Event triggered when a new TValue is available. /// - public event Action? Pub; + public event TValuePublishedHandler? Pub; /// /// Helper to invoke the Pub event. /// - 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 }); } /// diff --git a/lib/core/simd/SimdExtensions.cs b/lib/core/simd/SimdExtensions.cs index 9804128a..fa24c948 100644 --- a/lib/core/simd/SimdExtensions.cs +++ b/lib/core/simd/SimdExtensions.cs @@ -395,7 +395,7 @@ public static class SimdExtensions public static void Add(ReadOnlySpan left, ReadOnlySpan right, Span 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.Count) @@ -423,7 +423,7 @@ public static class SimdExtensions public static void Subtract(ReadOnlySpan left, ReadOnlySpan right, Span 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.Count) @@ -451,7 +451,7 @@ public static class SimdExtensions public static double DotProduct(this ReadOnlySpan a, ReadOnlySpan 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; diff --git a/lib/core/tbar/tbar.cs b/lib/core/tbar/tbar.cs index 1ac3a7ae..9c936212 100644 --- a/lib/core/tbar/tbar.cs +++ b/lib/core/tbar/tbar.cs @@ -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). /// [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); diff --git a/lib/core/tbarseries/TBarSeries.Tests.cs b/lib/core/tbarseries/TBarSeries.Tests.cs index 50e508a6..63d8e84e 100644 --- a/lib/core/tbarseries/TBarSeries.Tests.cs +++ b/lib/core/tbarseries/TBarSeries.Tests.cs @@ -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); diff --git a/lib/core/tbarseries/tbarseries.cs b/lib/core/tbarseries/tbarseries.cs index 406d4f3b..7d90a10b 100644 --- a/lib/core/tbarseries/tbarseries.cs +++ b/lib/core/tbarseries/tbarseries.cs @@ -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. /// +[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 { +#pragma warning disable MA0016 // Prefer using collection abstraction instead of implementation protected readonly List _t; protected readonly List _o; protected readonly List _h; protected readonly List _l; protected readonly List _c; protected readonly List _v; +#pragma warning restore MA0016 public string Name { get; set; } = "Bar"; - public event Action? 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 _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 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++) diff --git a/lib/core/tseries/ITValuePublisher.cs b/lib/core/tseries/ITValuePublisher.cs index 318990b8..0d924896 100644 --- a/lib/core/tseries/ITValuePublisher.cs +++ b/lib/core/tseries/ITValuePublisher.cs @@ -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); + /// /// Interface for objects that publish TValue updates. /// @@ -10,5 +18,5 @@ public interface ITValuePublisher /// /// Event triggered when a new TValue is available. /// - event Action Pub; + event TValuePublishedHandler? Pub; } diff --git a/lib/core/tseries/TSeries.Tests.cs b/lib/core/tseries/TSeries.Tests.cs index 459cb078..bd68482d 100644 --- a/lib/core/tseries/TSeries.Tests.cs +++ b/lib/core/tseries/TSeries.Tests.cs @@ -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); diff --git a/lib/core/tseries/tseries.cs b/lib/core/tseries/tseries.cs index 6489f90a..3f1016cf 100644 --- a/lib/core/tseries/tseries.cs +++ b/lib/core/tseries/tseries.cs @@ -13,12 +13,14 @@ namespace QuanTAlib; /// public class TSeries : IReadOnlyList, ITValuePublisher { +#pragma warning disable MA0016 // Prefer using collection abstraction instead of implementation protected readonly List _t; protected readonly List _v; +#pragma warning restore MA0016 public string Name { get; set; } = "Data"; - public event Action? Pub; + public event TValuePublishedHandler? Pub; public TSeries() : this(0) { @@ -30,10 +32,18 @@ public class TSeries : IReadOnlyList, ITValuePublisher _v = new List(capacity); } - public TSeries(List time, List values) + public TSeries(IReadOnlyList time, IReadOnlyList values) { - _t = time; - _v = values; + if (time is List timeList && values is List valueList) + { + _t = timeList; + _v = valueList; + } + else + { + _t = new List(time); + _v = new List(values); + } } public int Count @@ -98,7 +108,9 @@ public class TSeries : IReadOnlyList, 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) diff --git a/lib/core/tvalue/tvalue.cs b/lib/core/tvalue/tvalue.cs index c88e1af2..450ef84e 100644 --- a/lib/core/tvalue/tvalue.cs +++ b/lib/core/tvalue/tvalue.cs @@ -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). /// [SkipLocalsInit] +[StructLayout(LayoutKind.Auto)] public readonly record struct TValue(long Time, double Value) { public DateTime AsDateTime => new(Time, DateTimeKind.Utc); diff --git a/lib/feeds/gbm/ValidationHelper.cs b/lib/feeds/gbm/ValidationHelper.cs index b693b3d7..6f6ff7e0 100644 --- a/lib/feeds/gbm/ValidationHelper.cs +++ b/lib/feeds/gbm/ValidationHelper.cs @@ -13,7 +13,7 @@ public static class ValidationHelper public const double TulipTolerance = 1e-7; public const double RelativeTolerance = 0.005; - public static void VerifyData(TSeries qSeries, List sSeries, Func selector, int skip = 100, double tolerance = DefaultTolerance) + public static void VerifyData(TSeries qSeries, IReadOnlyList sSeries, Func 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(List qResults, List sSeries, Func selector, int skip = 100, double tolerance = DefaultTolerance) + public static void VerifyData(IReadOnlyList qResults, IReadOnlyList sSeries, Func 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 qResults, double[] tOutput, int lookback, int skip = 100, double tolerance = DefaultTolerance) + public static void VerifyData(IReadOnlyList 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 qResults, double[] tOutput, Range outRange, int lookback, int skip = 100, double tolerance = DefaultTolerance) + public static void VerifyData(IReadOnlyList qResults, double[] tOutput, Range outRange, int lookback, int skip = 100, double tolerance = DefaultTolerance) { int count = qResults.Count; int start = Math.Max(0, count - skip); diff --git a/lib/momentum/adx/Adx.cs b/lib/momentum/adx/Adx.cs index 758edeb1..202e1861 100644 --- a/lib/momentum/adx/Adx.cs +++ b/lib/momentum/adx/Adx.cs @@ -57,7 +57,7 @@ public sealed class Adx : ITValuePublisher /// public string Name { get; } - public event Action? Pub; + public event TValuePublishedHandler? Pub; /// /// 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; } diff --git a/lib/momentum/adxr/Adxr.cs b/lib/momentum/adxr/Adxr.cs index f4e88d17..9832ac79 100644 --- a/lib/momentum/adxr/Adxr.cs +++ b/lib/momentum/adxr/Adxr.cs @@ -30,7 +30,7 @@ public sealed class Adxr : ITValuePublisher /// public string Name { get; } - public event Action? Pub; + public event TValuePublishedHandler? Pub; /// /// 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]); } } + diff --git a/lib/momentum/ao/Ao.cs b/lib/momentum/ao/Ao.cs index c8f77d59..c9ea590b 100644 --- a/lib/momentum/ao/Ao.cs +++ b/lib/momentum/ao/Ao.cs @@ -31,7 +31,7 @@ public sealed class Ao : ITValuePublisher /// public string Name { get; } - public event Action? Pub; + public event TValuePublishedHandler? Pub; /// /// 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 high, ReadOnlySpan low, Span 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; diff --git a/lib/momentum/apo/Apo.cs b/lib/momentum/apo/Apo.cs index 89e598fd..df399e6e 100644 --- a/lib/momentum/apo/Apo.cs +++ b/lib/momentum/apo/Apo.cs @@ -26,13 +26,14 @@ public sealed class Apo : ITValuePublisher { private readonly Ema _emaFast; private readonly Ema _emaSlow; + private readonly TValuePublishedHandler _handler; /// /// Display name for the indicator. /// public string Name { get; } - public event Action? Pub; + public event TValuePublishedHandler? Pub; /// /// 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 /// Slow EMA period (default 26) public Apo(ITValuePublisher source, int fastPeriod = 12, int slowPeriod = 26) : this(fastPeriod, slowPeriod) { - source.Pub += (item) => Update(item); + source.Pub += _handler; } /// @@ -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); + } + /// /// Calculates APO for the entire series using a new instance. /// @@ -167,7 +174,7 @@ public sealed class Apo : ITValuePublisher public static void Calculate(ReadOnlySpan source, Span 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 fastEma = source.Length <= 1024 ? stackalloc double[source.Length] : new double[source.Length]; Span slowEma = source.Length <= 1024 ? stackalloc double[source.Length] : new double[source.Length]; diff --git a/lib/momentum/aroon/Aroon.cs b/lib/momentum/aroon/Aroon.cs index e3cd3a98..56652c46 100644 --- a/lib/momentum/aroon/Aroon.cs +++ b/lib/momentum/aroon/Aroon.cs @@ -32,7 +32,7 @@ public sealed class Aroon : ITValuePublisher /// public string Name { get; } - public event Action? Pub; + public event TValuePublishedHandler? Pub; /// /// 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]); } } + diff --git a/lib/momentum/aroonosc/AroonOsc.cs b/lib/momentum/aroonosc/AroonOsc.cs index 5fdfd111..91e86c1c 100644 --- a/lib/momentum/aroonosc/AroonOsc.cs +++ b/lib/momentum/aroonosc/AroonOsc.cs @@ -32,7 +32,7 @@ public sealed class AroonOsc : ITValuePublisher /// public string Name { get; } - public event Action? Pub; + public event TValuePublishedHandler? Pub; /// /// 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]); } } + diff --git a/lib/momentum/bop/Bop.cs b/lib/momentum/bop/Bop.cs index f3af090d..fd91a37d 100644 --- a/lib/momentum/bop/Bop.cs +++ b/lib/momentum/bop/Bop.cs @@ -32,7 +32,7 @@ public sealed class Bop : ITValuePublisher /// public static string Name => "Bop"; - public event Action? Pub; + public event TValuePublishedHandler? Pub; /// /// 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); } } + diff --git a/lib/momentum/cfb/Cfb.cs b/lib/momentum/cfb/Cfb.cs index 84184e32..0db66820 100644 --- a/lib/momentum/cfb/Cfb.cs +++ b/lib/momentum/cfb/Cfb.cs @@ -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? 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; } diff --git a/lib/momentum/dmx/Dmx.cs b/lib/momentum/dmx/Dmx.cs index 44a8ef03..9f54a088 100644 --- a/lib/momentum/dmx/Dmx.cs +++ b/lib/momentum/dmx/Dmx.cs @@ -23,7 +23,7 @@ public sealed class Dmx : ITValuePublisher private bool _isInitialized; public string Name { get; } - public event Action? 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)); diff --git a/lib/momentum/macd/Macd.cs b/lib/momentum/macd/Macd.cs index b5bec7f1..25352daf 100644 --- a/lib/momentum/macd/Macd.cs +++ b/lib/momentum/macd/Macd.cs @@ -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? 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); + } /// /// Calculates the MACD Line (Fast EMA - Slow EMA). @@ -108,7 +115,7 @@ public sealed class Macd : ITValuePublisher public static void Calculate(ReadOnlySpan source, Span 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.Shared.Rent(len); diff --git a/lib/momentum/rsi/Rsi.cs b/lib/momentum/rsi/Rsi.cs index 85aa6546..9f039e13 100644 --- a/lib/momentum/rsi/Rsi.cs +++ b/lib/momentum/rsi/Rsi.cs @@ -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 source) { foreach (var value in source) @@ -163,7 +170,7 @@ public sealed class Rsi : AbstractBase public static void Calculate(ReadOnlySpan source, Span 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)); diff --git a/lib/momentum/rsx/Rsx.cs b/lib/momentum/rsx/Rsx.cs index de0b7ebe..3123008f 100644 --- a/lib/momentum/rsx/Rsx.cs +++ b/lib/momentum/rsx/Rsx.cs @@ -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; /// /// Display name for the indicator. /// public string Name { get; } - public event Action? Pub; + public event TValuePublishedHandler? Pub; /// /// 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; } /// @@ -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 source, Span 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)); diff --git a/lib/momentum/vel/Vel.cs b/lib/momentum/vel/Vel.cs index 09cddeff..497ba635 100644 --- a/lib/momentum/vel/Vel.cs +++ b/lib/momentum/vel/Vel.cs @@ -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? 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 source, Span 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 pwma = source.Length <= 1024 ? stackalloc double[source.Length] : new double[source.Length]; Span wma = source.Length <= 1024 ? stackalloc double[source.Length] : new double[source.Length]; diff --git a/lib/quantalib.csproj b/lib/quantalib.csproj index 54880747..63e3c8d9 100644 --- a/lib/quantalib.csproj +++ b/lib/quantalib.csproj @@ -43,11 +43,29 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers + + + + all + runtime; build; native; contentfiles; analyzers + + + + all + runtime; build; native; contentfiles; analyzers + + diff --git a/lib/statistics/covariance/Covariance.cs b/lib/statistics/covariance/Covariance.cs index ad35cf40..c39b8df1 100644 --- a/lib/statistics/covariance/Covariance.cs +++ b/lib/statistics/covariance/Covariance.cs @@ -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(len); @@ -201,7 +201,7 @@ public sealed class Covariance : AbstractBase public static void Batch(ReadOnlySpan sourceX, ReadOnlySpan sourceY, Span 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)); diff --git a/lib/statistics/linreg/LinReg.cs b/lib/statistics/linreg/LinReg.cs index 100c0d50..40fb3b76 100644 --- a/lib/statistics/linreg/LinReg.cs +++ b/lib/statistics/linreg/LinReg.cs @@ -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 source, Span 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)); diff --git a/lib/statistics/median/Median.Validation.Tests.cs b/lib/statistics/median/Median.Validation.Tests.cs index 486d1db9..9225649d 100644 --- a/lib/statistics/median/Median.Validation.Tests.cs +++ b/lib/statistics/median/Median.Validation.Tests.cs @@ -88,7 +88,7 @@ public sealed class MedianValidationTests : IDisposable } } - private static double CalculateMedian(List sortedWindow) + private static double CalculateMedian(IReadOnlyList sortedWindow) { int count = sortedWindow.Count; if (count == 0) return 0; // Or NaN diff --git a/lib/statistics/median/Median.cs b/lib/statistics/median/Median.cs index 9b8a121f..9877171f 100644 --- a/lib/statistics/median/Median.cs +++ b/lib/statistics/median/Median.cs @@ -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; /// /// 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; } /// @@ -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 source, Span 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)); diff --git a/lib/statistics/skew/Skew.cs b/lib/statistics/skew/Skew.cs index 5cde21d2..dc1955ab 100644 --- a/lib/statistics/skew/Skew.cs +++ b/lib/statistics/skew/Skew.cs @@ -205,7 +205,7 @@ public sealed class Skew : AbstractBase public static void Batch(ReadOnlySpan source, Span 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)); diff --git a/lib/statistics/variance/Variance.cs b/lib/statistics/variance/Variance.cs index f2229227..3494c82b 100644 --- a/lib/statistics/variance/Variance.cs +++ b/lib/statistics/variance/Variance.cs @@ -181,7 +181,7 @@ public sealed class Variance : AbstractBase public static void Batch(ReadOnlySpan source, Span 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)); diff --git a/lib/trends/alma/Alma.cs b/lib/trends/alma/Alma.cs index 6f1226b8..1bc17b17 100644 --- a/lib/trends/alma/Alma.cs +++ b/lib/trends/alma/Alma.cs @@ -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? _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 diff --git a/lib/trends/bessel/Bessel.cs b/lib/trends/bessel/Bessel.cs index e4995c3b..1ddf2a6e 100644 --- a/lib/trends/bessel/Bessel.cs +++ b/lib/trends/bessel/Bessel.cs @@ -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? _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(); diff --git a/lib/trends/bilateral/Bilateral.Validation.Tests.cs b/lib/trends/bilateral/Bilateral.Validation.Tests.cs index dda25dca..42ac81da 100644 --- a/lib/trends/bilateral/Bilateral.Validation.Tests.cs +++ b/lib/trends/bilateral/Bilateral.Validation.Tests.cs @@ -111,7 +111,7 @@ public sealed class BilateralValidationTests : IDisposable _output.WriteLine("Bilateral Span validated successfully against Reference"); } - private List GetReferenceData(int period, double sigmaSRatio, double sigmaRMult) + private IReadOnlyList GetReferenceData(int period, double sigmaSRatio, double sigmaRMult) { var reference = new BilateralReference(period, sigmaSRatio, sigmaRMult); var results = new List(); @@ -180,7 +180,7 @@ public sealed class BilateralValidationTests : IDisposable return sumWeights < 1e-10 ? centerVal : sumWeightedSrc / sumWeights; } - private static double CalculateStDev(List values) + private static double CalculateStDev(IReadOnlyList values) { if (values.Count < 2) return 0; diff --git a/lib/trends/bilateral/Bilateral.cs b/lib/trends/bilateral/Bilateral.cs index e82f0d9b..8bca40a4 100644 --- a/lib/trends/bilateral/Bilateral.cs +++ b/lib/trends/bilateral/Bilateral.cs @@ -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; /// /// 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; } diff --git a/lib/trends/blma/Blma.Tests.cs b/lib/trends/blma/Blma.Tests.cs index bec22be4..e95f2e59 100644 --- a/lib/trends/blma/Blma.Tests.cs +++ b/lib/trends/blma/Blma.Tests.cs @@ -139,7 +139,8 @@ public class BlmaTests var input = new double[] { 1, 2, 3, 4, 5 }; var timestamps = new List(); - 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(); - blma.Pub += (item) => timestamps.Add(item.AsDateTime); + blma.Pub += (object? sender, TValueEventArgs args) => timestamps.Add(args.Value.AsDateTime); + blma.Prime(input); diff --git a/lib/trends/blma/Blma.Validation.Tests.cs b/lib/trends/blma/Blma.Validation.Tests.cs index 60260e90..bcd90111 100644 --- a/lib/trends/blma/Blma.Validation.Tests.cs +++ b/lib/trends/blma/Blma.Validation.Tests.cs @@ -42,7 +42,7 @@ public class BlmaValidationTests } } - private static List CalculateReference(TSeries source, int period) + private static IReadOnlyList CalculateReference(TSeries source, int period) { var result = new List(); var buffer = new List(); diff --git a/lib/trends/blma/Blma.cs b/lib/trends/blma/Blma.cs index db9db564..991d08eb 100644 --- a/lib/trends/blma/Blma.cs +++ b/lib/trends/blma/Blma.cs @@ -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; } diff --git a/lib/trends/butter/Butter.Validation.Tests.cs b/lib/trends/butter/Butter.Validation.Tests.cs index 9f566a37..452e985d 100644 --- a/lib/trends/butter/Butter.Validation.Tests.cs +++ b/lib/trends/butter/Butter.Validation.Tests.cs @@ -88,7 +88,7 @@ public class ButterValidationTests } } - private static List CalculateReference(TSeries source, int period) + private static IReadOnlyList CalculateReference(TSeries source, int period) { var result = new List(); diff --git a/lib/trends/butter/Butter.cs b/lib/trends/butter/Butter.cs index 8382e731..83f9d34d 100644 --- a/lib/trends/butter/Butter.cs +++ b/lib/trends/butter/Butter.cs @@ -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; } diff --git a/lib/trends/conv/Conv.cs b/lib/trends/conv/Conv.cs index c8e493e1..1756f09b 100644 --- a/lib/trends/conv/Conv.cs +++ b/lib/trends/conv/Conv.cs @@ -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? _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 source, Span 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)); diff --git a/lib/trends/dema/Dema.cs b/lib/trends/dema/Dema.cs index 8565671e..ba5d8ef3 100644 --- a/lib/trends/dema/Dema.cs +++ b/lib/trends/dema/Dema.cs @@ -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? _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 source, Span 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); } diff --git a/lib/trends/dwma/Dwma.cs b/lib/trends/dwma/Dwma.cs index 13a3450f..60f1be40 100644 --- a/lib/trends/dwma/Dwma.cs +++ b/lib/trends/dwma/Dwma.cs @@ -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 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 diff --git a/lib/trends/ema/Ema.cs b/lib/trends/ema/Ema.cs index c08f44b5..c34d92b3 100644 --- a/lib/trends/ema/Ema.cs +++ b/lib/trends/ema/Ema.cs @@ -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 /// Period for EMA calculation 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; } /// @@ -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) { diff --git a/lib/trends/hma/Hma.cs b/lib/trends/hma/Hma.cs index bad1858e..4451b1ad 100644 --- a/lib/trends/hma/Hma.cs +++ b/lib/trends/hma/Hma.cs @@ -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 source) { foreach (var value in source) @@ -141,7 +148,7 @@ public sealed class Hma : AbstractBase public static void Calculate(ReadOnlySpan source, Span 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)); diff --git a/lib/trends/htit/Htit.cs b/lib/trends/htit/Htit.cs index 739a2087..fb13b68b 100644 --- a/lib/trends/htit/Htit.cs +++ b/lib/trends/htit/Htit.cs @@ -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 source) { foreach (var value in source) @@ -255,7 +263,7 @@ public sealed class Htit : AbstractBase public static void Calculate(ReadOnlySpan source, Span 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; diff --git a/lib/trends/jma/Jma.cs b/lib/trends/jma/Jma.cs index 55343b0c..d23384a7 100644 --- a/lib/trends/jma/Jma.cs +++ b/lib/trends/jma/Jma.cs @@ -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 source) { foreach (var value in source) diff --git a/lib/trends/kama/Kama.cs b/lib/trends/kama/Kama.cs index 46513531..a33182df 100644 --- a/lib/trends/kama/Kama.cs +++ b/lib/trends/kama/Kama.cs @@ -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 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); diff --git a/lib/trends/lsma/Lsma.cs b/lib/trends/lsma/Lsma.cs index 90212e01..867ff45f 100644 --- a/lib/trends/lsma/Lsma.cs +++ b/lib/trends/lsma/Lsma.cs @@ -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 source, Span 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)); diff --git a/lib/trends/mama/Mama.Tests.cs b/lib/trends/mama/Mama.Tests.cs index 46b5ff85..4f571cbc 100644 --- a/lib/trends/mama/Mama.Tests.cs +++ b/lib/trends/mama/Mama.Tests.cs @@ -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)); diff --git a/lib/trends/mama/Mama.cs b/lib/trends/mama/Mama.cs index 48a1b952..251160dd 100644 --- a/lib/trends/mama/Mama.cs +++ b/lib/trends/mama/Mama.cs @@ -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; } diff --git a/lib/trends/mgdi/Mgdi.cs b/lib/trends/mgdi/Mgdi.cs index 29a7c9a5..58e19cf4 100644 --- a/lib/trends/mgdi/Mgdi.cs +++ b/lib/trends/mgdi/Mgdi.cs @@ -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; diff --git a/lib/trends/pwma/Pwma.cs b/lib/trends/pwma/Pwma.cs index a510f10d..66d4faa3 100644 --- a/lib/trends/pwma/Pwma.cs +++ b/lib/trends/pwma/Pwma.cs @@ -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 source, Span 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)); diff --git a/lib/trends/rma/Rma.cs b/lib/trends/rma/Rma.cs index e6867be6..c16f144f 100644 --- a/lib/trends/rma/Rma.cs +++ b/lib/trends/rma/Rma.cs @@ -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; } /// @@ -61,7 +61,7 @@ public sealed class Rma : AbstractBase { Last = new TValue(source.LastTime, Last.Value); } - source.Pub += (item) => Update(item); + source.Pub += Handle; } /// @@ -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); diff --git a/lib/trends/sma/Sma.cs b/lib/trends/sma/Sma.cs index c47ce3f4..660eaff6 100644 --- a/lib/trends/sma/Sma.cs +++ b/lib/trends/sma/Sma.cs @@ -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 source, Span 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)); diff --git a/lib/trends/ssf/Ssf.cs b/lib/trends/ssf/Ssf.cs index 1f2bd98a..77da41cf 100644 --- a/lib/trends/ssf/Ssf.cs +++ b/lib/trends/ssf/Ssf.cs @@ -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; } /// @@ -61,7 +64,7 @@ public sealed class Ssf : AbstractBase /// Period for SSF calculation 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 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; diff --git a/lib/trends/super/Super.cs b/lib/trends/super/Super.cs index 311094e7..52d92d00 100644 --- a/lib/trends/super/Super.cs +++ b/lib/trends/super/Super.cs @@ -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 /// public string Name => $"Super({_period},{_multiplier})"; - public event Action? Pub; + public event TValuePublishedHandler? Pub; /// /// 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; } diff --git a/lib/trends/t3/T3.Tests.cs b/lib/trends/t3/T3.Tests.cs index 16272ab9..457a90e4 100644 --- a/lib/trends/t3/T3.Tests.cs +++ b/lib/trends/t3/T3.Tests.cs @@ -173,7 +173,7 @@ public class T3Tests private class TestPublisher : ITValuePublisher { - public event Action? Pub; + public event TValuePublishedHandler? Pub; public int SubscriberCount => Pub?.GetInvocationList().Length ?? 0; } @@ -222,3 +222,4 @@ public class T3Tests Assert.Null(exception); } } + diff --git a/lib/trends/t3/T3.cs b/lib/trends/t3/T3.cs index bdd9c254..ee394cc2 100644 --- a/lib/trends/t3/T3.cs +++ b/lib/trends/t3/T3.cs @@ -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? _handler; + private TValuePublishedHandler? _handler; /// /// 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); + /// /// True if the T3 has been initialized (received at least one value). /// @@ -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; diff --git a/lib/trends/tema/Tema.cs b/lib/trends/tema/Tema.cs index 9fd7c0d8..46a88069 100644 --- a/lib/trends/tema/Tema.cs +++ b/lib/trends/tema/Tema.cs @@ -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); + /// /// Initializes the indicator state using the provided history. /// @@ -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 source, Span 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)); diff --git a/lib/trends/trima/Trima.cs b/lib/trends/trima/Trima.cs index fb9a60f4..526e0267 100644 --- a/lib/trends/trima/Trima.cs +++ b/lib/trends/trima/Trima.cs @@ -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 _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 source) { _sma1.Reset(); @@ -138,7 +140,7 @@ public sealed class Trima : AbstractBase, IDisposable public static void Batch(ReadOnlySpan source, Span 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)); diff --git a/lib/trends/usf/Usf.cs b/lib/trends/usf/Usf.cs index 84bc92dc..2db610e1 100644 --- a/lib/trends/usf/Usf.cs +++ b/lib/trends/usf/Usf.cs @@ -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; } /// @@ -57,7 +60,7 @@ public sealed class Usf : AbstractBase /// Period for USF calculation 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 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; diff --git a/lib/trends/vidya/Vidya.Validation.Tests.cs b/lib/trends/vidya/Vidya.Validation.Tests.cs index 3f504025..6479dbb1 100644 --- a/lib/trends/vidya/Vidya.Validation.Tests.cs +++ b/lib/trends/vidya/Vidya.Validation.Tests.cs @@ -66,7 +66,7 @@ public class VidyaValidationTests _output.WriteLine("VIDYA Batch validated successfully against reference implementation"); } - private static List CalculateVidyaReference(TSeries data, int period) + private static IReadOnlyList CalculateVidyaReference(TSeries data, int period) { var results = new List(); var prices = data.Select(x => x.Value).ToList(); diff --git a/lib/trends/vidya/Vidya.cs b/lib/trends/vidya/Vidya.cs index 417985f7..0004aac3 100644 --- a/lib/trends/vidya/Vidya.cs +++ b/lib/trends/vidya/Vidya.cs @@ -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? _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; diff --git a/lib/trends/wma/Wma.cs b/lib/trends/wma/Wma.cs index f5ab692d..71d3680c 100644 --- a/lib/trends/wma/Wma.cs +++ b/lib/trends/wma/Wma.cs @@ -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? _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 source) { if (source.Length == 0) return; @@ -248,7 +251,7 @@ public sealed class Wma : AbstractBase, IDisposable public static void Batch(ReadOnlySpan source, Span 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)); diff --git a/lib/volatility/atr/Atr.cs b/lib/volatility/atr/Atr.cs index 86637060..bf740e27 100644 --- a/lib/volatility/atr/Atr.cs +++ b/lib/volatility/atr/Atr.cs @@ -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; } /// @@ -46,7 +48,7 @@ public sealed class Atr : AbstractBase /// Period for ATR calculation public Atr(ITValuePublisher source, int period) : this(period) { - source.Pub += (item) => Update(item); + source.Pub += _handler; } /// @@ -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); + /// /// True if the ATR has warmed up and is providing valid results. /// @@ -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; } diff --git a/lib/volume/adl/Adl.Tests.cs b/lib/volume/adl/Adl.Tests.cs index 6c7913fc..c33d2e69 100644 --- a/lib/volume/adl/Adl.Tests.cs +++ b/lib/volume/adl/Adl.Tests.cs @@ -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); } diff --git a/lib/volume/adl/Adl.cs b/lib/volume/adl/Adl.cs index 4b8ce6fc..5e9948b8 100644 --- a/lib/volume/adl/Adl.cs +++ b/lib/volume/adl/Adl.cs @@ -33,7 +33,7 @@ public sealed class Adl : ITValuePublisher /// public static string Name => "ADL"; - public event Action? Pub; + public event TValuePublishedHandler? Pub; /// /// 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 high, ReadOnlySpan low, ReadOnlySpan close, ReadOnlySpan volume, Span 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 } } } + diff --git a/lib/volume/adosc/Adosc.cs b/lib/volume/adosc/Adosc.cs index 7008bf94..e810310e 100644 --- a/lib/volume/adosc/Adosc.cs +++ b/lib/volume/adosc/Adosc.cs @@ -32,7 +32,7 @@ public sealed class Adosc : ITValuePublisher /// public string Name { get; } - public event Action? Pub; + public event TValuePublishedHandler? Pub; /// /// 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 high, ReadOnlySpan low, ReadOnlySpan close, ReadOnlySpan volume, Span 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 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); } } + diff --git a/qodana.yaml b/qodana.yaml index 1aa46d80..639c45c2 100644 --- a/qodana.yaml +++ b/qodana.yaml @@ -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: - -#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 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 diff --git a/quantower/IndicatorExtensions.Tests.cs b/quantower/IndicatorExtensions.Tests.cs index 7566e1fd..2d0cea20 100644 --- a/quantower/IndicatorExtensions.Tests.cs +++ b/quantower/IndicatorExtensions.Tests.cs @@ -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()) { var series = new LineSeries("Test", Color.Blue, 1, style); for (int i = 0; i < 20; i++) series.AddValue();