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();