using Xunit;
using System.Reflection;
using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography;
namespace QuanTAlib;
///
/// Contains unit tests for bar-based indicators in QuanTAlib.
///
public class BarIndicatorTests
{
private readonly RandomNumberGenerator rng;
private const int SeriesLen = 1000;
private const int Corrections = 100;
///
/// Initializes a new instance of the BarIndicatorTests class.
///
public BarIndicatorTests()
{
rng = RandomNumberGenerator.Create();
}
private static readonly ITValue[] indicators = new ITValue[]
{
new Atr(period: 14),
// Add other TBar-based indicators here
};
///
/// Tests if the indicator produces consistent results when processing new and updated bars.
///
/// The indicator to test.
[Theory]
[MemberData(nameof(GetIndicators))]
public void IndicatorIsNew(ITValue indicator)
{
var indicator1 = indicator;
var indicator2 = indicator;
MethodInfo calcMethod = FindCalcMethod(indicator.GetType());
if (calcMethod == null)
{
throw new InvalidOperationException($"Calc method not found for indicator type: {indicator.GetType().Name}");
}
for (int i = 0; i < SeriesLen; i++)
{
TBar item1 = GenerateRandomBar(isNew: true);
InvokeCalc(indicator1, calcMethod, item1);
for (int j = 0; j < Corrections; j++)
{
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);
InvokeCalc(indicator2, calcMethod, item2);
Assert.Equal(indicator1.Value, indicator2.Value);
}
}
///
/// Finds the appropriate Calc method for the given indicator type.
///
/// The type of the indicator.
/// The MethodInfo for the Calc method.
[UnconditionalSuppressMessage("Trimming", "IL2072:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.",
Justification = "BaseType will have the same dynamic access requirements as the derived type in this reflection scenario.")]
private static MethodInfo FindCalcMethod([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] 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.Find(m =>
{
var parameters = m.GetParameters();
return parameters.Length == 1 && parameters[0].ParameterType == typeof(TBar);
});
// If not found, return the first method
return method ?? methods[0];
}
type = type.BaseType!;
}
return null!;
}
///
/// Invokes the Calc method on the given indicator with the provided input.
///
/// The indicator instance.
/// The Calc method to invoke.
/// The input TBar.
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}");
}
}
///
/// Generates a random TBar for testing purposes.
///
/// Indicates whether the generated bar should be marked as new.
/// A randomly generated TBar.
private TBar GenerateRandomBar(bool isNew)
{
double open = (GetRandomDouble() * 200) - 100;
double close = (GetRandomDouble() * 200) - 100;
double high = Math.Max(open, close) + (GetRandomDouble() * 10);
double low = Math.Min(open, close) - (GetRandomDouble() * 10);
long volume = GetRandomNumber(0, 10000);
return new TBar(Time: DateTime.Now, Open: open, High: high, Low: low, Close: close, Volume: volume, IsNew: isNew);
}
///
/// Generates a random double between 0 and 1.
///
/// A random double between 0 and 1.
private double GetRandomDouble()
{
byte[] bytes = new byte[8];
rng.GetBytes(bytes);
return (double)BitConverter.ToUInt64(bytes, 0) / ulong.MaxValue;
}
///
/// Generates a random integer between minValue (inclusive) and maxValue (exclusive).
///
/// The minimum value (inclusive).
/// The maximum value (exclusive).
/// A random integer between minValue and maxValue.
private int GetRandomNumber(int minValue, int maxValue)
{
byte[] randomBytes = new byte[4];
rng.GetBytes(randomBytes);
int randomInt = BitConverter.ToInt32(randomBytes, 0);
return Math.Abs(randomInt % (maxValue - minValue)) + minValue;
}
///
/// Provides the list of indicators for parameterized tests.
///
/// An enumerable of object arrays, each containing an indicator instance.
public static IEnumerable