Refactor event handling and improve argument validation across indicators

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