event tests

This commit is contained in:
Miha Kralj
2024-10-08 09:25:01 -07:00
parent b7b5a4a1bf
commit af234594cc
15 changed files with 391 additions and 200 deletions
+3 -2
View File
@@ -1,11 +1,12 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<NeutralLanguage>en-US</NeutralLanguage>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Deterministic>true</Deterministic> <Deterministic>true</Deterministic>
<LangVersion>preview</LangVersion> <NeutralLanguage>en-US</NeutralLanguage>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<DisableImplicitNamespaceImports>true</DisableImplicitNamespaceImports> <DisableImplicitNamespaceImports>true</DisableImplicitNamespaceImports>
-1
View File
@@ -1,6 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<RootNamespace>QuanTAlib.Tests</RootNamespace> <RootNamespace>QuanTAlib.Tests</RootNamespace>
<AssemblyName>QuanTAlib.Tests</AssemblyName> <AssemblyName>QuanTAlib.Tests</AssemblyName>
</PropertyGroup> </PropertyGroup>
+72
View File
@@ -0,0 +1,72 @@
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);
}
}
}
+92 -8
View File
@@ -4,14 +4,19 @@ using System.Diagnostics.CodeAnalysis;
namespace QuanTAlib; namespace QuanTAlib;
/// <summary>
/// Contains unit tests for bar-based indicators in QuanTAlib.
/// </summary>
[SuppressMessage("Security", "SCS0005:Weak random number generator.", Justification = "Acceptable for tests")] [SuppressMessage("Security", "SCS0005:Weak random number generator.", Justification = "Acceptable for tests")]
public class BarIndicatorTests public class BarIndicatorTests
{ {
private readonly Random rnd; private readonly Random rnd;
private const int SeriesLen = 1000; private const int SeriesLen = 1000;
private const int Corrections = 100; private const int Corrections = 100;
/// <summary>
/// Initializes a new instance of the BarIndicatorTests class.
/// </summary>
public BarIndicatorTests() public BarIndicatorTests()
{ {
rnd = new Random((int)DateTime.Now.Ticks); rnd = new Random((int)DateTime.Now.Ticks);
@@ -19,9 +24,14 @@ public class BarIndicatorTests
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] [Theory]
[MemberData(nameof(GetIndicators))] [MemberData(nameof(GetIndicators))]
public void IndicatorIsNew(ITValue indicator) public void IndicatorIsNew(ITValue indicator)
@@ -29,7 +39,7 @@ public class BarIndicatorTests
var indicator1 = indicator; var indicator1 = indicator;
var indicator2 = indicator; var indicator2 = indicator;
MethodInfo calcMethod = indicator.GetType().GetMethod("Calc")!; MethodInfo calcMethod = FindCalcMethod(indicator.GetType());
if (calcMethod == null) if (calcMethod == null)
{ {
throw new InvalidOperationException($"Calc method not found for indicator type: {indicator.GetType().Name}"); throw new InvalidOperationException($"Calc method not found for indicator type: {indicator.GetType().Name}");
@@ -37,22 +47,96 @@ public class BarIndicatorTests
for (int i = 0; i < SeriesLen; i++) 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); TBar item1 = GenerateRandomBar(isNew: true);
calcMethod.Invoke(indicator1, new object[] { item1 }); InvokeCalc(indicator1, calcMethod, item1);
for (int j = 0; j < Corrections; j++) 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); item1 = GenerateRandomBar(isNew: false);
calcMethod.Invoke(indicator1, new object[] { item1 }); InvokeCalc(indicator1, calcMethod, item1);
} }
var item2 = new TBar(item1.Time, item1.Open, item1.High, item1.Low, item1.Close, item1.Volume, IsNew: true); 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); 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() public static IEnumerable<object[]> GetIndicators()
{ {
return indicators.Select(indicator => new object[] { indicator }); return indicators.Select(indicator => new object[] { indicator });
+93 -44
View File
@@ -5,7 +5,6 @@ using System.Diagnostics.CodeAnalysis;
namespace QuanTAlib; namespace QuanTAlib;
[SuppressMessage("Security", "SCS0005:Weak random number generator.", Justification = "Acceptable for tests")] [SuppressMessage("Security", "SCS0005:Weak random number generator.", Justification = "Acceptable for tests")]
public class IndicatorTests public class IndicatorTests
{ {
private readonly Random rnd; private readonly Random rnd;
@@ -17,49 +16,56 @@ public class IndicatorTests
rnd = new Random((int)DateTime.Now.Ticks); rnd = new Random((int)DateTime.Now.Ticks);
} }
// skipcq: CS-R1055
private static readonly ITValue[] indicators = private static readonly ITValue[] indicators =
{ {
new Ema(period: 10, useSma: true), new Ema(period: 10, useSma: true),
new Alma(period: 14, offset: 0.85, sigma: 6), new Alma(period: 14, offset: 0.85, sigma: 6),
new Afirma(periods: 4, taps: 4, window: Afirma.WindowType.Blackman), new Afirma(periods: 4, taps: 4, window: Afirma.WindowType.Blackman),
new Convolution(new double[] { 1.0, 2, 3, 2, 1 }), new Convolution(new[] { 1.0, 2, 3, 2, 1 }),
new Dema(period: 14), new Dema(period: 14),
new Dsma(period: 14), new Dsma(period: 14),
new Dwma(period: 14), new Dwma(period: 14),
new Epma(period: 14), new Epma(period: 14),
new Frama(period: 14), new Frama(period: 14),
new Fwma(period: 14), new Fwma(period: 14),
new Gma(period: 14), new Gma(period: 14),
new Hma(period: 14), new Hma(period: 14),
new Hwma(period: 14), new Hwma(period: 14),
new Kama(period: 14), new Kama(period: 14),
new Mama(fastLimit: 0.5, slowLimit: 0.05), new Mama(fastLimit: 0.5, slowLimit: 0.05),
new Mgdi(period: 14), new Mgdi(period: 14),
new Mma(period: 14), new Mma(period: 14),
new Qema(), new Qema(),
new Rema(period: 14), new Rema(period: 14),
new Rma(period: 14), new Rma(period: 14),
new Sinema(period: 14), new Sinema(period: 14),
new Sma(period: 14), new Sma(period: 14),
new Smma(period: 14), new Smma(period: 14),
new T3(period: 14), new T3(period: 14),
new Tema(period: 14), new Tema(period: 14),
new Trima(period: 14), new Trima(period: 14),
new Vidya(shortPeriod: 14, longPeriod: 30, alpha: 0.2), new Vidya(shortPeriod: 14, longPeriod: 30, alpha: 0.2),
new Wma(period: 14), new Wma(period: 14),
new Zlema(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 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] [Theory]
@@ -69,7 +75,7 @@ public class IndicatorTests
var indicator1 = indicator; var indicator1 = indicator;
var indicator2 = indicator; var indicator2 = indicator;
MethodInfo calcMethod = indicator.GetType().GetMethod("Calc")!; MethodInfo calcMethod = FindCalcMethod(indicator.GetType());
if (calcMethod == null) if (calcMethod == null)
{ {
throw new InvalidOperationException($"Calc method not found for indicator type: {indicator.GetType().Name}"); throw new InvalidOperationException($"Calc method not found for indicator type: {indicator.GetType().Name}");
@@ -78,21 +84,64 @@ public class IndicatorTests
for (int i = 0; i < SeriesLen; i++) for (int i = 0; i < SeriesLen; i++)
{ {
TValue item1 = new(Time: DateTime.Now, Value: rnd.Next(-100, 100), IsNew: true); 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++) for (int j = 0; j < Corrections; j++)
{ {
item1 = new(Time: DateTime.Now, Value: rnd.Next(-100, 100), IsNew: false); 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); 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); 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() public static IEnumerable<object[]> GetIndicators()
{ {
return indicators.Select(indicator => new object[] { indicator }); return indicators.Select(indicator => new object[] { indicator });
+7 -3
View File
@@ -5,22 +5,26 @@ namespace QuanTAlib;
public class Frama : AbstractBase public class Frama : AbstractBase
{ {
private readonly int _period; private readonly int _period;
private readonly double _fc;
private readonly CircularBuffer _buffer; private readonly CircularBuffer _buffer;
private double _lastFrama; private double _lastFrama;
private double _prevLastFrama; private double _prevLastFrama;
public Frama(int period, double fc = 0.5) public Frama(int period)
{ {
if (period < 2) if (period < 2)
throw new ArgumentException("Period must be at least 2", nameof(period)); throw new ArgumentException("Period must be at least 2", nameof(period));
_period = period; _period = period;
_fc = fc;
_buffer = new CircularBuffer(period); _buffer = new CircularBuffer(period);
WarmupPeriod = period; WarmupPeriod = period;
} }
public Frama(object source, int period) : this(period)
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
}
public override void Init() public override void Init()
{ {
base.Init(); base.Init();
+6
View File
@@ -35,6 +35,12 @@ public class Jma : AbstractBase
Init(); Init();
} }
public Jma(object source, int period, double phase = 0, int vshort = 10) : this(period, phase, vshort)
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
}
public override void Init() public override void Init()
{ {
_upperBand = _lowerBand = _prevMa1 = _prevDet0 = _prevDet1 = _prevJma = 0.0; _upperBand = _lowerBand = _prevMa1 = _prevDet0 = _prevDet1 = _prevJma = 0.0;
+5 -1
View File
@@ -25,7 +25,11 @@ public class Rema : AbstractBase
WarmupPeriod = period; WarmupPeriod = period;
Init(); Init();
} }
public Rema(object source, int period, double lambda = 0.5) : this(period, lambda)
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
}
public override void Init() public override void Init()
{ {
base.Init(); base.Init();
-90
View File
@@ -1,90 +0,0 @@
namespace QuanTAlib;
/// <summary>
/// Provides a base implementation for financial indicators that work with bar data in the QuanTAlib library.
/// </summary>
/// <remarks>
/// This abstract class implements the iTValue interface and defines common properties
/// and methods used by inheriting indicator types. It handles the basic flow of
/// receiving bar data, performing calculations, and publishing results.
/// </remarks>
public abstract class AbstractBarBase : ITValue {
public DateTime Time { get; set; }
public double Value { get; set; }
public bool IsNew { get; set; }
public bool IsHot { get; set; }
public TBar Input { get; set; }
public String Name { get; set; } = "";
public int WarmupPeriod { get; set; }
public TValue Tick => new(Time, Value, IsNew, IsHot);
public event ValueSignal Pub = delegate { };
protected int _index;
protected double _lastValidValue;
protected AbstractBarBase() {
// Add parameters into constructor if needed
}
/// <summary>
/// Subscribes to bar data updates.
/// </summary>
/// <param name="source">The source of the bar data.</param>
/// <param name="args">The event arguments containing the bar data.</param>
public void Sub(object source, in TBarEventArgs args) => Calc(args.Bar);
/// <summary>
/// Initializes the indicator's state.
/// </summary>
public virtual void Init() {
_index = 0;
_lastValidValue = 0;
}
/// <summary>
/// Calculates the indicator value based on the input bar.
/// </summary>
/// <param name="input">The input bar data.</param>
/// <returns>A TValue containing the calculated result.</returns>
public virtual TValue Calc(TBar input) {
Input = input;
if (double.IsNaN(input.Close) || double.IsInfinity(input.Close)) {
return Process(new TValue(Time: input.Time, Value: GetLastValid(), IsNew: input.IsNew, IsHot: true));
}
this.Value = Calculation();
return Process(new TValue(Time: Input.Time, Value: this.Value, IsNew: Input.IsNew, IsHot: this.IsHot));
}
/// <summary>
/// Retrieves the last valid calculated value.
/// </summary>
/// <returns>The last valid value of the indicator.</returns>
protected virtual double GetLastValid() {
return this.Value;
}
/// <summary>
/// Manages the state of the indicator based on whether a new bar is being processed.
/// </summary>
/// <param name="isNew">Indicates whether the current input is a new bar.</param>
protected abstract void ManageState(bool isNew);
/// <summary>
/// Performs the actual calculation of the indicator value.
/// </summary>
/// <returns>The calculated indicator value.</returns>
protected abstract double Calculation();
/// <summary>
/// Processes the calculated value, updates the indicator's own state,
/// and publishes the result through an event.
/// </summary>
/// <param name="value">The calculated TValue to process.</param>
/// <returns>The processed TValue.</returns>
protected virtual TValue Process(TValue value) {
this.Time = value.Time;
this.Value = value.Value;
this.IsNew = value.IsNew;
this.IsHot = value.IsHot;
Pub?.Invoke(this, new ValueEventArgs(value));
return value;
}
}
+71 -12
View File
@@ -15,6 +15,9 @@ public abstract class AbstractBase : ITValue
public bool IsNew { get; set; } public bool IsNew { get; set; }
public bool IsHot { get; set; } public bool IsHot { get; set; }
public TValue Input { get; set; } public TValue Input { get; set; }
public TValue Input2 { get; set; }
public TBar BarInput { get; set; }
public TBar BarInput2 { get; set; }
public String Name { get; set; } = ""; public String Name { get; set; } = "";
public int WarmupPeriod { get; set; } public int WarmupPeriod { get; set; }
public TValue Tick => new(Time, Value, IsNew, IsHot); public TValue Tick => new(Time, Value, IsNew, IsHot);
@@ -34,6 +37,11 @@ public abstract class AbstractBase : ITValue
/// <param name="args">The argument containing the new data point.</param> /// <param name="args">The argument containing the new data point.</param>
public void Sub(object source, in ValueEventArgs args) => Calc(args.Tick); public void Sub(object source, in ValueEventArgs args) => Calc(args.Tick);
public void Sub(object source1, object source2, in ValueEventArgs args1, in ValueEventArgs args2) =>
Calc(args1.Tick, args2.Tick);
public void Sub(object source, in TBarEventArgs args) => Calc(args.Bar);
/// <summary> /// <summary>
/// Initializes the indicator's state. /// Initializes the indicator's state.
/// </summary> /// </summary>
@@ -43,24 +51,75 @@ public abstract class AbstractBase : ITValue
_lastValidValue = 0; _lastValidValue = 0;
} }
/// <summary>
/// Calculates the indicator value based on the input.
/// </summary>
/// <param name="input">The input value for the calculation.</param>
/// <returns>A TValue representing the calculated indicator value.</returns>
/// <remarks>
/// This method calls the specific Calculation() method where the actual implementation is.
/// If the input value is NaN or infinity, it returns the last valid value instead.
/// </remarks>
public virtual TValue Calc(TValue input) public virtual TValue Calc(TValue input)
{ {
Input = input; Input = input;
if (double.IsNaN(input.Value) || double.IsInfinity(input.Value)) Input2 = new(Time: Input.Time, Value: double.NaN, IsNew: Input.IsNew, IsHot: Input.IsHot);
return HandleErrorCalculations(input.Value, input.Time, input.IsNew);
}
public virtual TValue Calc(TBar barInput)
{
BarInput = barInput;
return HandleErrorCalculations(barInput.Close, barInput.Time, barInput.IsNew);
}
public virtual TValue Calc(TValue input1, TValue input2)
{
Input = input1;
Input2 = input2;
return HandleErrorCalculations(input1.Value, input2.Value, input1.Time, input1.IsNew);
}
public virtual TValue Calc(TBar input1, TBar input2)
{
BarInput = input1;
BarInput2 = input2;
return HandleErrorCalculations(input1.Close, input2.Close, input1.Time, input1.IsNew);
}
/// <summary>
/// Handles error calculations and invalid input values.
/// </summary>
/// <param name="value">The primary input value to check.</param>
/// <param name="time">The timestamp of the input.</param>
/// <param name="isNew">Indicates if the input is new.</param>
/// <returns>A TValue object with the calculated or last valid value.</returns>
/// <remarks>
/// This method checks for NaN or infinity in the input value. If an invalid value is detected,
/// it returns the last valid value. Otherwise, it proceeds with the calculation.
/// </remarks>
protected virtual TValue HandleErrorCalculations(double value, DateTime time, bool isNew)
{
if (double.IsNaN(value) || double.IsInfinity(value))
{ {
return Process(new TValue(input.Time, GetLastValid(), input.IsNew, input.IsHot)); return Process(new TValue(time, GetLastValid(), isNew, this.IsHot));
} }
this.Value = Calculation(); this.Value = Calculation();
return Process(new TValue(Time: Input.Time, Value: this.Value, IsNew: Input.IsNew, IsHot: this.IsHot)); return Process(new TValue(Time: time, Value: this.Value, IsNew: isNew, IsHot: this.IsHot));
}
/// <summary>
/// Handles error calculations for inputs with two values.
/// </summary>
/// <param name="value1">The first input value to check.</param>
/// <param name="value2">The second input value to check.</param>
/// <param name="time">The timestamp of the input.</param>
/// <param name="isNew">Indicates if the input is new.</param>
/// <returns>A TValue object with the calculated or last valid value.</returns>
/// <remarks>
/// This method checks for NaN or infinity in both input values. If any invalid value is detected,
/// it returns the last valid value. Otherwise, it proceeds with the calculation.
/// </remarks>
protected virtual TValue HandleErrorCalculations(double value1, double value2, DateTime time, bool isNew)
{
if (double.IsNaN(value1) || double.IsInfinity(value1) ||
double.IsNaN(value2) || double.IsInfinity(value2))
{
return Process(new TValue(time, GetLastValid(), isNew, this.IsHot));
}
this.Value = Calculation();
return Process(new TValue(Time: time, Value: this.Value, IsNew: isNew, IsHot: this.IsHot));
} }
/// <summary> /// <summary>
+2 -2
View File
@@ -52,12 +52,12 @@ public class TSeries : List<TValue>
var pubEvent = source.GetType().GetEvent("Pub"); var pubEvent = source.GetType().GetEvent("Pub");
if (pubEvent != null) if (pubEvent != null)
{ {
/*
var nameProperty = source.GetType().GetProperty("Name"); var nameProperty = source.GetType().GetProperty("Name");
if (nameProperty != null) { if (nameProperty != null) {
Name = nameProperty.GetValue(nameProperty)?.ToString()!; Name = nameProperty.GetValue(nameProperty)?.ToString()!;
} }
*/
pubEvent.AddEventHandler(source, new ValueSignal(Sub)); pubEvent.AddEventHandler(source, new ValueSignal(Sub));
} }
} }
+7 -7
View File
@@ -8,7 +8,7 @@ namespace QuanTAlib;
/// of the true range. The true range is the greatest of: current high - current low, /// of the true range. The true range is the greatest of: current high - current low,
/// absolute value of current high - previous close, or absolute value of current low - previous close. /// absolute value of current high - previous close, or absolute value of current low - previous close.
/// </remarks> /// </remarks>
public class Atr : AbstractBarBase { public class Atr : AbstractBase {
private readonly Ema _ma; private readonly Ema _ma;
private double _prevClose, _p_prevClose; private double _prevClose, _p_prevClose;
@@ -72,22 +72,22 @@ public class Atr : AbstractBarBase {
/// as the true range. /// as the true range.
/// </remarks> /// </remarks>
protected override double Calculation() { protected override double Calculation() {
ManageState(Input.IsNew); ManageState(BarInput.IsNew);
double trueRange = Math.Max( double trueRange = Math.Max(
Math.Max( Math.Max(
Input.High - Input.Low, BarInput.High - BarInput.Low,
Math.Abs(Input.High - _prevClose) Math.Abs(BarInput.High - _prevClose)
), ),
Math.Abs(Input.Low - _prevClose) Math.Abs(BarInput.Low - _prevClose)
); );
if (_index < 2) { if (_index < 2) {
trueRange = Input.High - Input.Low; trueRange = BarInput.High - BarInput.Low;
} }
TValue emaTrueRange = _ma.Calc(new TValue(Input.Time, trueRange, Input.IsNew)); TValue emaTrueRange = _ma.Calc(new TValue(Input.Time, trueRange, Input.IsNew));
IsHot = _ma.IsHot; IsHot = _ma.IsHot;
_prevClose = Input.Close; _prevClose = BarInput.Close;
return emaTrueRange.Value; return emaTrueRange.Value;
} }
+13 -9
View File
@@ -10,15 +10,19 @@ QuanTAlib.Formatters.Initialize();
#!csharp #!csharp
Sma ma1 = new(6); TSeries input = new();
Gmean ma2 = new (6); Sma ma1 = new (6);
Hmean ma3 = new (6); Sma ma2 = new (input, 6);
double[] input = new[]{1.0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,12,13,14,15,16,17,18,19,20}; Random random = new Random();
for (int i=0; i<input.Length; i++) {
double out1 = ma1.Calc(input[i]);
double out2 = ma2.Calc(input[i]);
double out3 = ma3.Calc(input[i]);
Console.WriteLine($"{input[i]:F2}\t {out1:F2}\t {out2:F2}\t {out3:F2}"); for (int i = 0; i < 100; i++) {
double randomValue = random.NextDouble() * 100;
input.Add(randomValue);
ma1.Calc(randomValue);
} }
#!csharp
display(ma1);
display(ma2);
+1 -1
View File
@@ -7,7 +7,7 @@ public class AtrIndicator : IndicatorBarBase
public int Period { get; set; } = 20; public int Period { get; set; } = 20;
private Atr? atr; private Atr? atr;
protected override AbstractBarBase QuanTAlib => atr!; protected override AbstractBase QuanTAlib => atr!;
public override string ShortName => $"ATR {Period}"; public override string ShortName => $"ATR {Period}";
public AtrIndicator() public AtrIndicator()
{ {
+18 -19
View File
@@ -19,7 +19,7 @@ public abstract class IndicatorBarBase : Indicator, IWatchlistIndicator
// LineSeries.LineSeries(string, Color, int, LineStyle)' // LineSeries.LineSeries(string, Color, int, LineStyle)'
protected LineSeries? Series; protected LineSeries? Series;
protected abstract AbstractBarBase QuanTAlib { get; } protected abstract AbstractBase QuanTAlib { get; }
int IWatchlistIndicator.MinHistoryDepths => 0; int IWatchlistIndicator.MinHistoryDepths => 0;
@@ -80,7 +80,7 @@ public abstract class IndicatorBarBase : Indicator, IWatchlistIndicator
int barX = (int)converter.GetChartX(Time(i)); int barX = (int)converter.GetChartX(Time(i));
int barY = (int)converter.GetChartY(Series![i]); int barY = (int)converter.GetChartY(Series![i]);
int halfBarWidth = CurrentChart.BarsWidth / 2; int halfBarWidth = CurrentChart.BarsWidth / 2;
Point point = new Point(barX + halfBarWidth, barY); Point point = new(barX + halfBarWidth, barY);
allPoints.Add(point); allPoints.Add(point);
} }
@@ -94,22 +94,21 @@ public abstract class IndicatorBarBase : Indicator, IWatchlistIndicator
{ {
if (allPoints.Count < 2) { return; } if (allPoints.Count < 2) { return; }
using (Pen defaultPen = new(Series!.Color, Series.Width) { DashStyle = ConvertLineStyleToDashStyle(Series.Style) }) using Pen defaultPen = new(Series!.Color, Series.Width) { DashStyle = ConvertLineStyleToDashStyle(Series.Style) };
using (Pen coldPen = new(Series!.Color, Series.Width) { DashStyle = DashStyle.Dot }) using Pen coldPen = new(Series!.Color, Series.Width) { DashStyle = DashStyle.Dot };
{
// Draw the hot part
if (hotCount > 0)
{
var hotPoints = allPoints.Take(Math.Min(hotCount + 1, allPoints.Count)).ToArray();
gr.DrawCurve(defaultPen, hotPoints, 0, hotPoints.Length - 1, (float)0.1);
}
// Draw the cold part // Draw the hot part
if (ShowColdValues && hotCount < allPoints.Count) if (hotCount > 0)
{ {
var coldPoints = allPoints.Skip(Math.Max(0, hotCount)).ToArray(); var hotPoints = allPoints.Take(Math.Min(hotCount + 1, allPoints.Count)).ToArray();
gr.DrawCurve(coldPen, coldPoints, 0, coldPoints.Length - 1, (float)0.1); gr.DrawCurve(defaultPen, hotPoints, 0, hotPoints.Length - 1, (float)0.1);
} }
// Draw the cold part
if (ShowColdValues && hotCount < allPoints.Count)
{
var coldPoints = allPoints.Skip(Math.Max(0, hotCount)).ToArray();
gr.DrawCurve(coldPen, coldPoints, 0, coldPoints.Length - 1, (float)0.1);
} }
} }
private static DashStyle ConvertLineStyleToDashStyle(LineStyle lineStyle) private static DashStyle ConvertLineStyleToDashStyle(LineStyle lineStyle)
@@ -125,9 +124,9 @@ public abstract class IndicatorBarBase : Indicator, IWatchlistIndicator
} }
protected static void DrawText(Graphics gr, string text, Rectangle clientRect) protected static void DrawText(Graphics gr, string text, Rectangle clientRect)
{ {
Font font = new Font("Inter", 8); Font font = new("Inter", 8);
SizeF textSize = gr.MeasureString(text, font); SizeF textSize = gr.MeasureString(text, font);
RectangleF textRect = new RectangleF(clientRect.Left + 5, RectangleF textRect = new(clientRect.Left + 5,
clientRect.Bottom - textSize.Height - 10, clientRect.Bottom - textSize.Height - 10,
textSize.Width + 10, textSize.Height + 10); textSize.Width + 10, textSize.Height + 10);
gr.FillRectangle(SystemBrushes.ControlDarkDark, textRect); gr.FillRectangle(SystemBrushes.ControlDarkDark, textRect);