using Xunit; using System.Reflection; using System.Diagnostics.CodeAnalysis; namespace QuanTAlib; /// /// Contains unit tests for bar-based indicators in QuanTAlib. /// [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; /// /// Initializes a new instance of the BarIndicatorTests class. /// public BarIndicatorTests() { rnd = new Random((int)DateTime.Now.Ticks); } 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. 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!; } /// /// 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 = 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); } /// /// 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 }); } }