2024-09-30 06:46:07 -07:00
using Xunit ;
using System.Reflection ;
2024-10-05 19:36:01 -07:00
using System.Diagnostics.CodeAnalysis ;
2024-10-08 17:34:58 -07:00
using System.Security.Cryptography ;
2024-09-30 06:46:07 -07:00
2024-10-05 19:36:01 -07:00
namespace QuanTAlib ;
2024-10-07 21:41:26 -07:00
/// <summary>
/// Contains unit tests for bar-based indicators in QuanTAlib.
/// </summary>
2024-10-05 19:36:01 -07:00
public class BarIndicatorTests
2024-09-30 06:46:07 -07:00
{
2024-10-08 17:34:58 -07:00
private readonly RandomNumberGenerator rng ;
2024-10-05 19:36:01 -07:00
private const int SeriesLen = 1000 ;
private const int Corrections = 100 ;
2024-10-07 21:41:26 -07:00
/// <summary>
/// Initializes a new instance of the BarIndicatorTests class.
/// </summary>
2024-10-05 19:36:01 -07:00
public BarIndicatorTests ()
2024-09-30 06:46:07 -07:00
{
2024-10-08 17:34:58 -07:00
rng = RandomNumberGenerator . Create ();
2024-10-05 19:36:01 -07:00
}
2024-09-30 06:46:07 -07:00
2024-10-06 14:44:43 -07:00
private static readonly ITValue [] indicators = new ITValue []
2024-10-05 19:36:01 -07:00
{
2024-10-07 21:41:26 -07:00
new Atr ( period : 14 ),
2024-10-16 18:28:06 -07:00
2024-10-07 21:41:26 -07:00
// Add other TBar-based indicators here
2024-10-05 19:36:01 -07:00
};
2024-09-30 06:46:07 -07:00
2024-10-07 21:41:26 -07:00
/// <summary>
/// Tests if the indicator produces consistent results when processing new and updated bars.
/// </summary>
/// <param name="indicator">The indicator to test.</param>
2024-10-05 19:36:01 -07:00
[Theory]
[MemberData(nameof(GetIndicators))]
2024-10-06 14:44:43 -07:00
public void IndicatorIsNew ( ITValue indicator )
2024-10-05 19:36:01 -07:00
{
var indicator1 = indicator ;
var indicator2 = indicator ;
2024-10-07 21:41:26 -07:00
MethodInfo calcMethod = FindCalcMethod ( indicator . GetType ());
2024-10-05 19:36:01 -07:00
if ( calcMethod == null )
2024-09-30 06:46:07 -07:00
{
2024-10-06 14:44:43 -07:00
throw new InvalidOperationException ( $"Calc method not found for indicator type: {indicator.GetType().Name}" );
2024-09-30 06:46:07 -07:00
}
2024-10-05 19:36:01 -07:00
for ( int i = 0 ; i < SeriesLen ; i ++)
2024-09-30 06:46:07 -07:00
{
2024-10-07 21:41:26 -07:00
TBar item1 = GenerateRandomBar ( isNew : true );
InvokeCalc ( indicator1 , calcMethod , item1 );
2024-10-05 19:36:01 -07:00
for ( int j = 0 ; j < Corrections ; j ++)
{
2024-10-07 21:41:26 -07:00
item1 = GenerateRandomBar ( isNew : false );
InvokeCalc ( indicator1 , calcMethod , item1 );
2024-10-05 19:36:01 -07:00
}
var item2 = new TBar ( item1 . Time , item1 . Open , item1 . High , item1 . Low , item1 . Close , item1 . Volume , IsNew : true );
2024-10-07 21:41:26 -07:00
InvokeCalc ( indicator2 , calcMethod , item2 );
2024-10-05 19:36:01 -07:00
Assert . Equal ( indicator1 . Value , indicator2 . Value );
2024-09-30 06:46:07 -07:00
}
}
2024-10-05 19:36:01 -07:00
2024-10-07 21:41:26 -07:00
/// <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>
2025-10-21 18:50:23 -07:00
[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 )
2024-10-07 21:41:26 -07:00
{
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
2024-10-16 18:28:06 -07:00
var method = methods . Find ( m =>
2024-10-07 21:41:26 -07:00
{
var parameters = m . GetParameters ();
return parameters . Length == 1 && parameters [ 0 ]. ParameterType == typeof ( TBar );
});
// If not found, return the first method
2024-10-16 18:28:06 -07:00
return method ?? methods [ 0 ];
2024-10-07 21:41:26 -07:00
}
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 )
{
2024-11-03 23:03:24 +00:00
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 );
2024-10-08 17:34:58 -07:00
long volume = GetRandomNumber ( 0 , 10000 );
2024-10-07 21:41:26 -07:00
return new TBar ( Time : DateTime . Now , Open : open , High : high , Low : low , Close : close , Volume : volume , IsNew : isNew );
}
2024-10-08 17:34:58 -07:00
/// <summary>
/// Generates a random double between 0 and 1.
/// </summary>
/// <returns>A random double between 0 and 1.</returns>
private double GetRandomDouble ()
{
byte [] bytes = new byte [ 8 ];
rng . GetBytes ( bytes );
return ( double ) BitConverter . ToUInt64 ( bytes , 0 ) / ulong . MaxValue ;
}
/// <summary>
/// Generates a random integer between minValue (inclusive) and maxValue (exclusive).
/// </summary>
/// <param name="minValue">The minimum value (inclusive).</param>
/// <param name="maxValue">The maximum value (exclusive).</param>
/// <returns>A random integer between minValue and maxValue.</returns>
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 ;
}
2024-10-07 21:41:26 -07:00
/// <summary>
/// Provides the list of indicators for parameterized tests.
/// </summary>
/// <returns>An enumerable of object arrays, each containing an indicator instance.</returns>
2024-10-05 19:36:01 -07:00
public static IEnumerable < object []> GetIndicators ()
{
return indicators . Select ( indicator => new object [] { indicator });
}
}