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 GetIndicators() { return indicators.Select(indicator => new object[] { indicator }); } }