mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-22 12:38:06 +00:00
Merge branch 'dev'
This commit is contained in:
+10
-5
@@ -1,22 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<RootNamespace>QuanTAlib.Tests</RootNamespace>
|
||||
<AssemblyName>QuanTAlib.Tests</AssemblyName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.0-pre.35">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="xunit.runner.console" Version="2.9.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
|
||||
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
|
||||
|
||||
|
||||
<PackageReference Include="Skender.Stock.Indicators" Version="2.5.0" />
|
||||
<PackageReference Include="TALib.NETCore" Version="0.4.4" />
|
||||
<PackageReference Include="Tulip.NETCore" Version="0.8.0.1" />
|
||||
@@ -29,6 +33,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<ProjectReference Include="..\lib\quantalib.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class EventingTests
|
||||
{
|
||||
[Fact]
|
||||
public void VerifyEventBasedCalculations()
|
||||
{
|
||||
// Create a random number generator with a fixed seed for reproducibility
|
||||
var random = new Random(42);
|
||||
|
||||
// Create an input series to hold our random values
|
||||
var input = new TSeries();
|
||||
int p = 10;
|
||||
|
||||
// Create a list of indicator pairs (direct calculation and event-based)
|
||||
var indicators = new List<(AbstractBase Direct, AbstractBase EventBased)>
|
||||
{
|
||||
(new Afirma(p,p,Afirma.WindowType.BlackmanHarris), new Afirma(input, p,p,Afirma.WindowType.BlackmanHarris)),
|
||||
(new Alma(p), new Alma(input, p)),
|
||||
(new Convolution([1,2,3,2,1]), new Convolution(input, [1,2,3,2,1])),
|
||||
(new Dema(p), new Dema(input, p)),
|
||||
(new Dsma(p), new Dsma(input, p)),
|
||||
(new Dwma(p), new Dwma(input, p)),
|
||||
(new Ema(p), new Ema(input, p)),
|
||||
(new Epma(p), new Epma(input, p)),
|
||||
(new Frama(p), new Frama(input, p)),
|
||||
(new Fwma(p), new Fwma(input, p)),
|
||||
(new Gma(p), new Gma(input, p)),
|
||||
(new Hma(p), new Hma(input, p)),
|
||||
(new Htit(), new Htit(input)),
|
||||
(new Hwma(p), new Hwma(input, p)),
|
||||
(new Jma(p), new Jma(input, p)),
|
||||
(new Kama(p), new Kama(input, p)),
|
||||
(new Ltma(gamma: 0.2), new Ltma(input, gamma: 0.2)),
|
||||
(new Maaf(p), new Maaf(input, p)),
|
||||
(new Mama(p), new Mama(input, p)),
|
||||
(new Mgdi(p), new Mgdi(input, p)),
|
||||
(new Mma(p), new Mma(input, p)),
|
||||
(new Qema(k1: 0.2, k2: 0.2, k3: 0.2, k4: 0.2), new Qema(input, k1: 0.2, k2: 0.2, k3: 0.2, k4: 0.2)),
|
||||
(new Rema(p), new Rema(input, p)),
|
||||
(new Rma(p), new Rma(input, p)),
|
||||
|
||||
(new Sma(p), new Sma(input, p)),
|
||||
(new Wma(p), new Wma(input, p)),
|
||||
(new Rma(p), new Rma(input, p)),
|
||||
|
||||
(new Tema(p), new Tema(input, p)),
|
||||
(new Kama(2, 30, 6), new Kama(input, 2, 30, 6)),
|
||||
|
||||
(new Zlema(p), new Zlema(input, p))
|
||||
};
|
||||
|
||||
// Generate 200 random values and feed them to both direct and event-based indicators
|
||||
for (int i = 0; i< 200; i++)
|
||||
{
|
||||
double randomValue = random.NextDouble() * 100;
|
||||
input.Add(randomValue);
|
||||
|
||||
// Calculate direct indicators
|
||||
foreach (var (direct, _) in indicators)
|
||||
{
|
||||
direct.Calc(randomValue);
|
||||
}
|
||||
}
|
||||
|
||||
// Compare the results of direct and event-based calculations
|
||||
foreach (var (direct, eventBased) in indicators)
|
||||
{
|
||||
Assert.Equal(direct.Value, eventBased.Value, 9);
|
||||
}
|
||||
}
|
||||
}
|
||||
+95
-11
@@ -4,55 +4,139 @@ using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Contains unit tests for bar-based indicators in QuanTAlib.
|
||||
/// </summary>
|
||||
[SuppressMessage("Security", "SCS0005:Weak random number generator.", Justification = "Acceptable for tests")]
|
||||
|
||||
public class BarIndicatorTests
|
||||
{
|
||||
private readonly Random rnd;
|
||||
private const int SeriesLen = 1000;
|
||||
private const int Corrections = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the BarIndicatorTests class.
|
||||
/// </summary>
|
||||
public BarIndicatorTests()
|
||||
{
|
||||
rnd = new Random((int)DateTime.Now.Ticks);
|
||||
}
|
||||
|
||||
private static readonly iTValue[] indicators = new iTValue[]
|
||||
private static readonly ITValue[] indicators = new ITValue[]
|
||||
{
|
||||
new Atr(period: 14),
|
||||
new Atr(period: 14),
|
||||
// Add other TBar-based indicators here
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the indicator produces consistent results when processing new and updated bars.
|
||||
/// </summary>
|
||||
/// <param name="indicator">The indicator to test.</param>
|
||||
[Theory]
|
||||
[MemberData(nameof(GetIndicators))]
|
||||
public void IndicatorIsNew(iTValue indicator)
|
||||
public void IndicatorIsNew(ITValue indicator)
|
||||
{
|
||||
var indicator1 = indicator;
|
||||
var indicator2 = indicator;
|
||||
|
||||
MethodInfo calcMethod = indicator.GetType().GetMethod("Calc")!;
|
||||
MethodInfo calcMethod = FindCalcMethod(indicator.GetType());
|
||||
if (calcMethod == null)
|
||||
{
|
||||
throw new Exception($"Calc method not found for indicator type: {indicator.GetType().Name}");
|
||||
throw new InvalidOperationException($"Calc method not found for indicator type: {indicator.GetType().Name}");
|
||||
}
|
||||
|
||||
for (int i = 0; i < SeriesLen; i++)
|
||||
{
|
||||
TBar item1 = new(Time: DateTime.Now, Open: rnd.Next(-100, 100), High: rnd.Next(-100, 100), Low: rnd.Next(-100, 100), Close: rnd.Next(-100, 100), Volume: rnd.Next(-1000, 1000), IsNew: true);
|
||||
calcMethod.Invoke(indicator1, new object[] { item1 });
|
||||
TBar item1 = GenerateRandomBar(isNew: true);
|
||||
InvokeCalc(indicator1, calcMethod, item1);
|
||||
|
||||
for (int j = 0; j < Corrections; j++)
|
||||
{
|
||||
item1 = new(Time: DateTime.Now, Open: rnd.Next(-100, 100), High: rnd.Next(-100, 100), Low: rnd.Next(-100, 100), Close: rnd.Next(-100, 100), Volume: rnd.Next(-1000, 1000), IsNew: false);
|
||||
calcMethod.Invoke(indicator1, new object[] { item1 });
|
||||
item1 = GenerateRandomBar(isNew: false);
|
||||
InvokeCalc(indicator1, calcMethod, item1);
|
||||
}
|
||||
|
||||
var item2 = new TBar(item1.Time, item1.Open, item1.High, item1.Low, item1.Close, item1.Volume, IsNew: true);
|
||||
calcMethod.Invoke(indicator2, new object[] { item2 });
|
||||
InvokeCalc(indicator2, calcMethod, item2);
|
||||
|
||||
Assert.Equal(indicator1.Value, indicator2.Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the appropriate Calc method for the given indicator type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of the indicator.</param>
|
||||
/// <returns>The MethodInfo for the Calc method.</returns>
|
||||
private static MethodInfo FindCalcMethod(Type type)
|
||||
{
|
||||
while (type != null && type != typeof(object))
|
||||
{
|
||||
var methods = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)
|
||||
.Where(m => m.Name == "Calc")
|
||||
.ToList();
|
||||
|
||||
if (methods.Count > 0)
|
||||
{
|
||||
// Prefer the method with TBar parameter
|
||||
var method = methods.FirstOrDefault(m =>
|
||||
{
|
||||
var parameters = m.GetParameters();
|
||||
return parameters.Length == 1 && parameters[0].ParameterType == typeof(TBar);
|
||||
});
|
||||
|
||||
// If not found, return the first method
|
||||
return method ?? methods.First();
|
||||
}
|
||||
|
||||
type = type.BaseType!;
|
||||
}
|
||||
return null!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the Calc method on the given indicator with the provided input.
|
||||
/// </summary>
|
||||
/// <param name="indicator">The indicator instance.</param>
|
||||
/// <param name="calcMethod">The Calc method to invoke.</param>
|
||||
/// <param name="input">The input TBar.</param>
|
||||
private static void InvokeCalc(ITValue indicator, MethodInfo calcMethod, TBar input)
|
||||
{
|
||||
var parameters = calcMethod.GetParameters();
|
||||
if (parameters.Length == 1)
|
||||
{
|
||||
calcMethod.Invoke(indicator, new object[] { input });
|
||||
}
|
||||
else if (parameters.Length == 2)
|
||||
{
|
||||
calcMethod.Invoke(indicator, new object[] { input, double.NaN });
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid number of parameters for Calc method in indicator type: {indicator.GetType().Name}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a random TBar for testing purposes.
|
||||
/// </summary>
|
||||
/// <param name="isNew">Indicates whether the generated bar should be marked as new.</param>
|
||||
/// <returns>A randomly generated TBar.</returns>
|
||||
private TBar GenerateRandomBar(bool isNew)
|
||||
{
|
||||
double open = rnd.NextDouble() * 200 - 100;
|
||||
double close = rnd.NextDouble() * 200 - 100;
|
||||
double high = Math.Max(open, close) + rnd.NextDouble() * 10;
|
||||
double low = Math.Min(open, close) - rnd.NextDouble() * 10;
|
||||
long volume = rnd.Next(0, 10000);
|
||||
|
||||
return new TBar(Time: DateTime.Now, Open: open, High: high, Low: low, Close: close, Volume: volume, IsNew: isNew);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides the list of indicators for parameterized tests.
|
||||
/// </summary>
|
||||
/// <returns>An enumerable of object arrays, each containing an indicator instance.</returns>
|
||||
public static IEnumerable<object[]> GetIndicators()
|
||||
{
|
||||
return indicators.Select(indicator => new object[] { indicator });
|
||||
|
||||
+98
-49
@@ -5,7 +5,6 @@ using System.Diagnostics.CodeAnalysis;
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SuppressMessage("Security", "SCS0005:Weak random number generator.", Justification = "Acceptable for tests")]
|
||||
|
||||
public class IndicatorTests
|
||||
{
|
||||
private readonly Random rnd;
|
||||
@@ -17,82 +16,132 @@ public class IndicatorTests
|
||||
rnd = new Random((int)DateTime.Now.Ticks);
|
||||
}
|
||||
|
||||
private static readonly iTValue[] indicators =
|
||||
[
|
||||
// skipcq: CS-R1055
|
||||
private static readonly ITValue[] indicators =
|
||||
{
|
||||
new Ema(period: 10, useSma: true),
|
||||
new Alma(period: 14, offset: 0.85, sigma: 6),
|
||||
new Afirma(periods: 4, taps: 4, window: Afirma.WindowType.Blackman),
|
||||
new Convolution(new double[] { 1.0, 2, 3, 2, 1 }),
|
||||
new Dema(period: 14),
|
||||
new Dsma(period: 14),
|
||||
new Dwma(period: 14),
|
||||
new Epma(period: 14),
|
||||
new Frama(period: 14),
|
||||
new Fwma(period: 14),
|
||||
new Gma(period: 14),
|
||||
new Hma(period: 14),
|
||||
new Hwma(period: 14),
|
||||
new Kama(period: 14),
|
||||
new Mama(fastLimit: 0.5, slowLimit: 0.05),
|
||||
new Mgdi(period: 14),
|
||||
new Mma(period: 14),
|
||||
new Qema(),
|
||||
new Rema(period: 14),
|
||||
new Rma(period: 14),
|
||||
new Sinema(period: 14),
|
||||
new Sma(period: 14),
|
||||
new Smma(period: 14),
|
||||
new T3(period: 14),
|
||||
new Tema(period: 14),
|
||||
new Trima(period: 14),
|
||||
new Vidya(shortPeriod: 14, longPeriod: 30, alpha: 0.2),
|
||||
new Wma(period: 14),
|
||||
new Zlema(period: 14),
|
||||
new Entropy(period: 14),
|
||||
new Kurtosis(period: 14),
|
||||
new Max(period: 14, decay: 0.01),
|
||||
new Min(period: 14, decay: 0.01),
|
||||
new Median(period: 14),
|
||||
new Mode(period: 14),
|
||||
new Percentile(period: 14, percent: 50),
|
||||
new Skew(period: 14),
|
||||
new Stddev(period: 14),
|
||||
new Variance(period: 14),
|
||||
new Zscore(period: 14)
|
||||
new Alma(period: 14, offset: 0.85, sigma: 6),
|
||||
new Afirma(periods: 4, taps: 4, window: Afirma.WindowType.Blackman),
|
||||
new Convolution(new[] { 1.0, 2, 3, 2, 1 }),
|
||||
new Dema(period: 14),
|
||||
new Dsma(period: 14),
|
||||
new Dwma(period: 14),
|
||||
new Epma(period: 14),
|
||||
new Frama(period: 14),
|
||||
new Fwma(period: 14),
|
||||
new Gma(period: 14),
|
||||
new Hma(period: 14),
|
||||
new Hwma(period: 14),
|
||||
new Kama(period: 14),
|
||||
new Mama(fastLimit: 0.5, slowLimit: 0.05),
|
||||
new Mgdi(period: 14),
|
||||
new Mma(period: 14),
|
||||
new Qema(),
|
||||
new Rema(period: 14),
|
||||
new Rma(period: 14),
|
||||
new Sinema(period: 14),
|
||||
new Sma(period: 14),
|
||||
new Smma(period: 14),
|
||||
new T3(period: 14),
|
||||
new Tema(period: 14),
|
||||
new Trima(period: 14),
|
||||
new Vidya(shortPeriod: 14, longPeriod: 30, alpha: 0.2),
|
||||
new Wma(period: 14),
|
||||
new Zlema(period: 14),
|
||||
|
||||
];
|
||||
new Curvature(period: 14),
|
||||
new Entropy(period: 14),
|
||||
new Kurtosis(period: 14),
|
||||
new Max(period: 14, decay: 0.01),
|
||||
new Median(period: 14),
|
||||
new Min(period: 14, decay: 0.01),
|
||||
new Median(period: 14),
|
||||
new Mode(period: 14),
|
||||
new Percentile(period: 14, percent: 50),
|
||||
new Skew(period: 14),
|
||||
new Slope(period: 14),
|
||||
new Stddev(period: 14),
|
||||
new Variance(period: 14),
|
||||
new Zscore(period: 14),
|
||||
|
||||
new Historical(period: 14),
|
||||
new Realized(period: 14)
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(GetIndicators))]
|
||||
public void IndicatorIsNew(iTValue indicator)
|
||||
public void IndicatorIsNew(ITValue indicator)
|
||||
{
|
||||
var indicator1 = indicator;
|
||||
var indicator2 = indicator;
|
||||
|
||||
MethodInfo calcMethod = indicator.GetType().GetMethod("Calc")!;
|
||||
MethodInfo calcMethod = FindCalcMethod(indicator.GetType());
|
||||
if (calcMethod == null)
|
||||
{
|
||||
throw new Exception($"Calc method not found for indicator type: {indicator.GetType().Name}");
|
||||
throw new InvalidOperationException($"Calc method not found for indicator type: {indicator.GetType().Name}");
|
||||
}
|
||||
|
||||
for (int i = 0; i < SeriesLen; i++)
|
||||
{
|
||||
TValue item1 = new(Time: DateTime.Now, Value: rnd.Next(-100, 100), IsNew: true);
|
||||
calcMethod.Invoke(indicator1, new object[] { item1 });
|
||||
InvokeCalc(indicator1, calcMethod, item1);
|
||||
|
||||
for (int j = 0; j < Corrections; j++)
|
||||
{
|
||||
item1 = new(Time: DateTime.Now, Value: rnd.Next(-100, 100), IsNew: false);
|
||||
calcMethod.Invoke(indicator1, new object[] { item1 });
|
||||
InvokeCalc(indicator1, calcMethod, item1);
|
||||
}
|
||||
|
||||
var item2 = new TValue(item1.Time, item1.Value, IsNew: true);
|
||||
calcMethod.Invoke(indicator2, new object[] { item2 });
|
||||
InvokeCalc(indicator2, calcMethod, item2);
|
||||
|
||||
Assert.Equal(indicator1.Value, indicator2.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private static MethodInfo FindCalcMethod(Type type)
|
||||
{
|
||||
while (type != null && type != typeof(object))
|
||||
{
|
||||
var methods = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)
|
||||
.Where(m => m.Name == "Calc")
|
||||
.ToList();
|
||||
|
||||
if (methods.Count > 0)
|
||||
{
|
||||
// Prefer the method with TValue parameter
|
||||
var method = methods.FirstOrDefault(m =>
|
||||
{
|
||||
var parameters = m.GetParameters();
|
||||
return parameters.Length == 1 && parameters[0].ParameterType == typeof(TValue);
|
||||
});
|
||||
|
||||
// If not found, return the first method
|
||||
return method ?? methods.First();
|
||||
}
|
||||
|
||||
type = type.BaseType!;
|
||||
}
|
||||
return null!;
|
||||
}
|
||||
|
||||
private static void InvokeCalc(ITValue indicator, MethodInfo calcMethod, TValue input)
|
||||
{
|
||||
var parameters = calcMethod.GetParameters();
|
||||
if (parameters.Length == 1)
|
||||
{
|
||||
calcMethod.Invoke(indicator, new object[] { input });
|
||||
}
|
||||
else if (parameters.Length == 2)
|
||||
{
|
||||
calcMethod.Invoke(indicator, new object[] { input, double.NaN });
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid number of parameters for Calc method in indicator type: {indicator.GetType().Name}");
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> GetIndicators()
|
||||
{
|
||||
return indicators.Select(indicator => new object[] { indicator });
|
||||
|
||||
@@ -12,7 +12,8 @@ public class SkenderTests
|
||||
private readonly GbmFeed feed;
|
||||
private readonly Random rnd;
|
||||
private readonly double range;
|
||||
private int period, iterations;
|
||||
private int period;
|
||||
private readonly int iterations;
|
||||
private readonly IEnumerable<Quote> quotes;
|
||||
|
||||
|
||||
@@ -288,7 +289,6 @@ public class SkenderTests
|
||||
{
|
||||
for (int run = 0; run < iterations; run++)
|
||||
{
|
||||
//period = rnd.Next(50) + 5;
|
||||
Mama ma = new(fastLimit: 0.5, slowLimit: 0.05);
|
||||
TSeries QL = new();
|
||||
foreach (TBar item in feed)
|
||||
@@ -335,12 +335,16 @@ public class SkenderTests
|
||||
TSeries QL = new();
|
||||
foreach (TBar item in bars) { QL.Add(ma.Calc(item)); }
|
||||
|
||||
var SK = quotes.GetAtr(lookbackPeriods: period).Select(i => i.Atr.Null2NaN()!);
|
||||
Assert.Equal(QL.Length, QL.Length);
|
||||
var atrValues = quotes.GetAtr(lookbackPeriods: period).Select(i => i.Atr.Null2NaN()!);
|
||||
const int AdditionalPeriods = 500;
|
||||
|
||||
<<<<<<< HEAD
|
||||
for (int i = QL.Length - 1; i > period + 500; i--)
|
||||
=======
|
||||
for (int i = QL.Length - 1; i > period + AdditionalPeriods; i--)
|
||||
>>>>>>> dev
|
||||
{
|
||||
Assert.InRange(SK.ElementAt(i) - QL[i].Value, -range, range);
|
||||
Assert.InRange(atrValues.ElementAt(i) - QL[i].Value, -range, range);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user