mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-21 12:08:05 +00:00
refactor: clean up Ema and EmaVector tests for consistency, update TValue equality check
This commit is contained in:
@@ -195,6 +195,8 @@ public class EmaTests
|
|||||||
series.Add(bar.Time, bar.Close);
|
series.Add(bar.Time, bar.Close);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Assert.True(series.Count > 0);
|
||||||
|
|
||||||
// Calculate iteratively
|
// Calculate iteratively
|
||||||
var iterativeResults = new TSeries();
|
var iterativeResults = new TSeries();
|
||||||
foreach (var item in series)
|
foreach (var item in series)
|
||||||
@@ -234,7 +236,6 @@ public class EmaTests
|
|||||||
// Feed some valid values
|
// Feed some valid values
|
||||||
ema.Update(new TValue(DateTime.UtcNow, 100));
|
ema.Update(new TValue(DateTime.UtcNow, 100));
|
||||||
ema.Update(new TValue(DateTime.UtcNow, 110));
|
ema.Update(new TValue(DateTime.UtcNow, 110));
|
||||||
double valueBeforeNaN = ema.Value;
|
|
||||||
|
|
||||||
// Feed NaN - should use last valid value (110)
|
// Feed NaN - should use last valid value (110)
|
||||||
var resultAfterNaN = ema.Update(new TValue(DateTime.UtcNow, double.NaN));
|
var resultAfterNaN = ema.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ public class EmaValidationTests
|
|||||||
private readonly TBarSeries _bars;
|
private readonly TBarSeries _bars;
|
||||||
private readonly TSeries _data;
|
private readonly TSeries _data;
|
||||||
private readonly List<Quote> _skenderQuotes;
|
private readonly List<Quote> _skenderQuotes;
|
||||||
private readonly Random _rnd = new(42);
|
|
||||||
private readonly ITestOutputHelper _output;
|
private readonly ITestOutputHelper _output;
|
||||||
|
|
||||||
public EmaValidationTests(ITestOutputHelper output)
|
public EmaValidationTests(ITestOutputHelper output)
|
||||||
@@ -35,7 +34,7 @@ public class EmaValidationTests
|
|||||||
{
|
{
|
||||||
_skenderQuotes.Add(new Quote
|
_skenderQuotes.Add(new Quote
|
||||||
{
|
{
|
||||||
Date = new DateTime(_bars.Open.Times[i]),
|
Date = new DateTime(_bars.Open.Times[i], DateTimeKind.Utc),
|
||||||
Open = (decimal)_bars.Open[i].Value,
|
Open = (decimal)_bars.Open[i].Value,
|
||||||
High = (decimal)_bars.High[i].Value,
|
High = (decimal)_bars.High[i].Value,
|
||||||
Low = (decimal)_bars.Low[i].Value,
|
Low = (decimal)_bars.Low[i].Value,
|
||||||
@@ -60,7 +59,7 @@ public class EmaValidationTests
|
|||||||
var sResult = _skenderQuotes.GetEma(period).ToList();
|
var sResult = _skenderQuotes.GetEma(period).ToList();
|
||||||
|
|
||||||
// Compare last 100 records
|
// Compare last 100 records
|
||||||
VerifyData(qResult, sResult, period);
|
VerifyData(qResult, sResult);
|
||||||
}
|
}
|
||||||
_output.WriteLine("EMA validated successfully against Skender");
|
_output.WriteLine("EMA validated successfully against Skender");
|
||||||
}
|
}
|
||||||
@@ -90,7 +89,7 @@ public class EmaValidationTests
|
|||||||
int lookback = TALib.Functions.EmaLookback(period);
|
int lookback = TALib.Functions.EmaLookback(period);
|
||||||
|
|
||||||
// Compare last 100 records
|
// Compare last 100 records
|
||||||
VerifyData_Talib(qResult, output, outRange, lookback, period);
|
VerifyData_Talib(qResult, output, outRange, lookback);
|
||||||
}
|
}
|
||||||
_output.WriteLine("EMA validated successfully against TA-Lib");
|
_output.WriteLine("EMA validated successfully against TA-Lib");
|
||||||
}
|
}
|
||||||
@@ -119,12 +118,12 @@ public class EmaValidationTests
|
|||||||
var tResult = outputs[0];
|
var tResult = outputs[0];
|
||||||
|
|
||||||
// Compare last 100 records
|
// Compare last 100 records
|
||||||
VerifyData(qResult, tResult.ToList(), period);
|
VerifyData(qResult, tResult.ToList());
|
||||||
}
|
}
|
||||||
_output.WriteLine("EMA validated successfully against Tulip");
|
_output.WriteLine("EMA validated successfully against Tulip");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void VerifyData(TSeries qSeries, List<double> tSeries, int period)
|
private static void VerifyData(TSeries qSeries, List<double> tSeries)
|
||||||
{
|
{
|
||||||
// Ensure we have enough data
|
// Ensure we have enough data
|
||||||
Assert.Equal(qSeries.Count, tSeries.Count);
|
Assert.Equal(qSeries.Count, tSeries.Count);
|
||||||
@@ -136,13 +135,13 @@ public class EmaValidationTests
|
|||||||
{
|
{
|
||||||
double qValue = qSeries[i].Value;
|
double qValue = qSeries[i].Value;
|
||||||
double tValue = tSeries[i];
|
double tValue = tSeries[i];
|
||||||
if (tValue == 0) continue;
|
if (Math.Abs(tValue) < 1e-10) continue;
|
||||||
|
|
||||||
Assert.Equal(tValue, qValue, 1e-6);
|
Assert.Equal(tValue, qValue, 1e-6);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void VerifyData(TSeries qSeries, List<EmaResult> sSeries, int period)
|
private static void VerifyData(TSeries qSeries, List<EmaResult> sSeries)
|
||||||
{
|
{
|
||||||
// Ensure we have enough data
|
// Ensure we have enough data
|
||||||
Assert.Equal(qSeries.Count, sSeries.Count);
|
Assert.Equal(qSeries.Count, sSeries.Count);
|
||||||
@@ -163,7 +162,7 @@ public class EmaValidationTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void VerifyData_Talib(TSeries qSeries, double[] tOutput, Range outRange, int lookback, int period)
|
private static void VerifyData_Talib(TSeries qSeries, double[] tOutput, Range outRange, int lookback)
|
||||||
{
|
{
|
||||||
int count = qSeries.Count;
|
int count = qSeries.Count;
|
||||||
int skip = count - 100; // Last 100 records
|
int skip = count - 100; // Last 100 records
|
||||||
|
|||||||
+29
-18
@@ -3,28 +3,39 @@ using System.Runtime.InteropServices;
|
|||||||
|
|
||||||
namespace QuanTAlib;
|
namespace QuanTAlib;
|
||||||
|
|
||||||
internal struct EmaState
|
|
||||||
{
|
|
||||||
public double Ema { get; set; }
|
|
||||||
public double E { get; set; }
|
|
||||||
public bool IsHot { get; set; }
|
|
||||||
|
|
||||||
public static EmaState New() => new() { Ema = 0, E = 1.0, IsHot = false };
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Exponential Moving Average (EMA) - IIR filter with exponential warmup compensator.
|
/// EMA: Exponential Moving Average
|
||||||
/// Provides valid output from first bar with O(1) complexity.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Algorithm uses exponential smoothing with compensator for immediate valid results.
|
/// EMA needs very short history buffer and calculates the EMA value using just the
|
||||||
/// Reference: https://github.com/mihakralj/pinescript/blob/main/indicators/trends_IIR/ema.md
|
/// previous EMA value. The weight of the new datapoint (alpha) is alpha = 2 / (period + 1)
|
||||||
|
///
|
||||||
|
/// Key characteristics:
|
||||||
|
/// - Uses no buffer, relying only on the previous EMA value.
|
||||||
|
/// - The weight of new data points is calculated as alpha = 2 / (period + 1).
|
||||||
|
/// - Provides a balance between responsiveness and smoothing. No overshooting. Significant lag
|
||||||
|
///
|
||||||
|
/// Calculation method:
|
||||||
|
/// This implementation can use SMA for the first Period bars as a seeding value for EMA when useSma is true.
|
||||||
|
///
|
||||||
|
/// Sources:
|
||||||
|
/// - https://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:moving_averages
|
||||||
|
/// - https://www.investopedia.com/ask/answers/122314/what-exponential-moving-average-ema-formula-and-how-ema-calculated.asp
|
||||||
|
/// - https://blog.fugue88.ws/archives/2017-01/The-correct-way-to-start-an-Exponential-Moving-Average-EMA
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public class Ema
|
public class Ema
|
||||||
{
|
{
|
||||||
|
private struct State
|
||||||
|
{
|
||||||
|
public double Ema;
|
||||||
|
public double E;
|
||||||
|
public bool IsHot;
|
||||||
|
public static State New() => new() { Ema = 0, E = 1.0, IsHot = false };
|
||||||
|
}
|
||||||
|
|
||||||
private readonly double _alpha;
|
private readonly double _alpha;
|
||||||
private EmaState _state = EmaState.New();
|
private State _state = State.New();
|
||||||
private EmaState _p_state = EmaState.New();
|
private State _p_state = State.New();
|
||||||
private double _lastValidValue;
|
private double _lastValidValue;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -81,7 +92,7 @@ public class Ema
|
|||||||
/// Assumes input has already been validated via GetValidValue().
|
/// Assumes input has already been validated via GetValidValue().
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
internal static double Compute(double input, double alpha, ref EmaState state)
|
private static double Compute(double input, double alpha, ref State state)
|
||||||
{
|
{
|
||||||
state.Ema += alpha * (input - state.Ema);
|
state.Ema += alpha * (input - state.Ema);
|
||||||
|
|
||||||
@@ -144,7 +155,7 @@ public class Ema
|
|||||||
var sourceTimes = source.Times;
|
var sourceTimes = source.Times;
|
||||||
|
|
||||||
// Local state for batch processing
|
// Local state for batch processing
|
||||||
EmaState state = _state;
|
State state = _state;
|
||||||
|
|
||||||
for (int i = 0; i < len; i++)
|
for (int i = 0; i < len; i++)
|
||||||
{
|
{
|
||||||
@@ -180,7 +191,7 @@ public class Ema
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
_state = EmaState.New();
|
_state = State.New();
|
||||||
_p_state = _state;
|
_p_state = _state;
|
||||||
_lastValidValue = 0;
|
_lastValidValue = 0;
|
||||||
Value = default;
|
Value = default;
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ public class EmaVectorTests
|
|||||||
|
|
||||||
for (int i = 0; i < len; i++)
|
for (int i = 0; i < len; i++)
|
||||||
{
|
{
|
||||||
var tVal = new TValue(new DateTime(t[i]), v[i]);
|
var tVal = new TValue(new DateTime(t[i], DateTimeKind.Utc), v[i]);
|
||||||
var streamRes = emaVectorStream.Update(tVal);
|
var streamRes = emaVectorStream.Update(tVal);
|
||||||
|
|
||||||
for (int j = 0; j < periods.Length; j++)
|
for (int j = 0; j < periods.Length; j++)
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ public class EmaVector
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Current EMA values for all periods.
|
/// Current EMA values for all periods.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TValue[] Values { get; private set; }
|
public ReadOnlySpan<TValue> Values => _values;
|
||||||
|
|
||||||
|
private readonly TValue[] _values;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes EmaVector with specified periods.
|
/// Initializes EmaVector with specified periods.
|
||||||
@@ -37,7 +39,7 @@ public class EmaVector
|
|||||||
_Es = new double[_count];
|
_Es = new double[_count];
|
||||||
_p_emas = new double[_count];
|
_p_emas = new double[_count];
|
||||||
_p_Es = new double[_count];
|
_p_Es = new double[_count];
|
||||||
Values = new TValue[_count];
|
_values = new TValue[_count];
|
||||||
|
|
||||||
for (int i = 0; i < _count; i++)
|
for (int i = 0; i < _count; i++)
|
||||||
{
|
{
|
||||||
@@ -59,7 +61,7 @@ public class EmaVector
|
|||||||
_Es = new double[_count];
|
_Es = new double[_count];
|
||||||
_p_emas = new double[_count];
|
_p_emas = new double[_count];
|
||||||
_p_Es = new double[_count];
|
_p_Es = new double[_count];
|
||||||
Values = new TValue[_count];
|
_values = new TValue[_count];
|
||||||
|
|
||||||
for (int i = 0; i < _count; i++)
|
for (int i = 0; i < _count; i++)
|
||||||
{
|
{
|
||||||
@@ -100,7 +102,7 @@ public class EmaVector
|
|||||||
ResetAt(i);
|
ResetAt(i);
|
||||||
}
|
}
|
||||||
_lastValidValue = 0;
|
_lastValidValue = 0;
|
||||||
Array.Clear(Values);
|
Array.Clear(_values);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -177,7 +179,7 @@ public class EmaVector
|
|||||||
// Store result
|
// Store result
|
||||||
for (int j = 0; j < vecCount; j++)
|
for (int j = 0; j < vecCount; j++)
|
||||||
{
|
{
|
||||||
Values[i + j] = new TValue(input.Time, vecResult[j]);
|
_values[i + j] = new TValue(input.Time, vecResult[j]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -198,10 +200,10 @@ public class EmaVector
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Values[i] = new TValue(input.Time, result);
|
_values[i] = new TValue(input.Time, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Values;
|
return _values;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -305,7 +307,7 @@ public class EmaVector
|
|||||||
resultSeries[i] = new TSeries(tLists[i], vLists[i]);
|
resultSeries[i] = new TSeries(tLists[i], vLists[i]);
|
||||||
var lastT = CollectionsMarshal.AsSpan(tLists[i])[len - 1];
|
var lastT = CollectionsMarshal.AsSpan(tLists[i])[len - 1];
|
||||||
var lastV = CollectionsMarshal.AsSpan(vLists[i])[len - 1];
|
var lastV = CollectionsMarshal.AsSpan(vLists[i])[len - 1];
|
||||||
Values[i] = new TValue(lastT, lastV);
|
_values[i] = new TValue(lastT, lastV);
|
||||||
}
|
}
|
||||||
|
|
||||||
return resultSeries;
|
return resultSeries;
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ public readonly struct TValue : IEquatable<TValue>
|
|||||||
public override string ToString() => $"[{AsDateTime:yyyy-MM-dd HH:mm:ss}, {Value:F2}]";
|
public override string ToString() => $"[{AsDateTime:yyyy-MM-dd HH:mm:ss}, {Value:F2}]";
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public bool Equals(TValue other) => Time == other.Time && Value == other.Value;
|
public bool Equals(TValue other) => Time == other.Time && Math.Abs(Value - other.Value) < 1e-9;
|
||||||
|
|
||||||
public override bool Equals(object? obj) => obj is TValue other && Equals(other);
|
public override bool Equals(object? obj) => obj is TValue other && Equals(other);
|
||||||
public override int GetHashCode() => HashCode.Combine(Time, Value);
|
public override int GetHashCode() => HashCode.Combine(Time, Value);
|
||||||
|
|||||||
Reference in New Issue
Block a user