feat: Introduce ITValuePublisher interface and refactor indicators for event-driven value updates.

This commit is contained in:
Miha Kralj
2025-12-07 14:36:22 -08:00
parent 3b146b68bd
commit 3734a1c5f6
16 changed files with 572 additions and 177 deletions
+58 -15
View File
@@ -29,12 +29,12 @@ public class TemaTests
{
var tema = new Tema(10);
Assert.Equal(0, tema.Value.Value);
Assert.Equal(0, tema.Last.Value);
TValue result = tema.Update(new TValue(DateTime.UtcNow, 100));
Assert.True(result.Value > 0);
Assert.Equal(result.Value, tema.Value.Value);
Assert.Equal(result.Value, tema.Last.Value);
}
[Fact]
@@ -43,10 +43,10 @@ public class TemaTests
var tema = new Tema(10);
tema.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
double value1 = tema.Value;
double value1 = tema.Last.Value;
tema.Update(new TValue(DateTime.UtcNow, 105), isNew: true);
double value2 = tema.Value;
double value2 = tema.Last.Value;
// Values should change with new bars
Assert.NotEqual(value1, value2);
@@ -59,10 +59,10 @@ public class TemaTests
tema.Update(new TValue(DateTime.UtcNow, 100));
tema.Update(new TValue(DateTime.UtcNow, 110), isNew: true);
double beforeUpdate = tema.Value;
double beforeUpdate = tema.Last.Value;
tema.Update(new TValue(DateTime.UtcNow, 120), isNew: false);
double afterUpdate = tema.Value;
double afterUpdate = tema.Last.Value;
// Update should change the value
Assert.NotEqual(beforeUpdate, afterUpdate);
@@ -75,16 +75,16 @@ public class TemaTests
tema.Update(new TValue(DateTime.UtcNow, 100));
tema.Update(new TValue(DateTime.UtcNow, 105));
double valueBefore = tema.Value;
double valueBefore = tema.Last.Value;
tema.Reset();
Assert.Equal(0, tema.Value.Value);
Assert.Equal(0, tema.Last.Value);
// After reset, should accept new values
tema.Update(new TValue(DateTime.UtcNow, 50));
Assert.NotEqual(0, tema.Value.Value);
Assert.NotEqual(valueBefore, tema.Value.Value);
Assert.NotEqual(0, tema.Last.Value);
Assert.NotEqual(valueBefore, tema.Last.Value);
}
[Fact]
@@ -92,12 +92,12 @@ public class TemaTests
{
var tema = new Tema(10);
Assert.Equal(0, tema.Value.Value);
Assert.Equal(0, tema.Last.Value);
Assert.False(tema.IsHot);
tema.Update(new TValue(DateTime.UtcNow, 100));
Assert.NotEqual(0, tema.Value.Value);
Assert.NotEqual(0, tema.Last.Value);
}
[Fact]
@@ -152,7 +152,7 @@ public class TemaTests
}
// Remember TEMA state after 10 values
double temaAfterTen = tema.Value;
double temaAfterTen = tema.Last.Value;
// Generate 9 corrections with isNew=false (different values)
for (int i = 0; i < 9; i++)
@@ -253,10 +253,11 @@ public class TemaTests
public void Tema_SpanCalc_ZeroAllocation()
{
double[] source = new double[10000];
double[] output = new double[10000];
var rng = new Random(42); // nosemgrep
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
for (int i = 0; i < source.Length; i++)
source[i] = rng.NextDouble() * 100;
source[i] = gbm.Next().Close;
// Warm up
Tema.Calculate(source.AsSpan(), output.AsSpan(), 100);
@@ -264,4 +265,46 @@ public class TemaTests
// This test verifies the method runs without throwing
Assert.True(double.IsFinite(output[^1]));
}
[Fact]
public void Tema_AllModes_ProduceSameResult()
{
// Arrange
int period = 10;
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
// 1. Batch Mode
var batchSeries = Tema.Calculate(series, period);
double expected = batchSeries.Last.Value;
// 2. Span Mode
var tValues = series.Values.ToArray();
var spanInput = new ReadOnlySpan<double>(tValues);
var spanOutput = new double[tValues.Length];
Tema.Calculate(spanInput, spanOutput, period);
double spanResult = spanOutput[^1];
// 3. Streaming Mode
var streamingInd = new Tema(period);
for (int i = 0; i < series.Count; i++)
{
streamingInd.Update(series[i]);
}
double streamingResult = streamingInd.Last.Value;
// 4. Eventing Mode
var pubSource = new TSeries();
var eventingInd = new Tema(pubSource, period);
for (int i = 0; i < series.Count; i++)
{
pubSource.Add(series[i]);
}
double eventingResult = eventingInd.Last.Value;
// Assert
Assert.Equal(expected, spanResult, precision: 9);
Assert.Equal(expected, streamingResult, precision: 9);
Assert.Equal(expected, eventingResult, precision: 9);
}
}
+13 -6
View File
@@ -24,7 +24,7 @@ namespace QuanTAlib;
/// which is faster than the standard EMA convergence (3/alpha steps).
/// </remarks>
[SkipLocalsInit]
public sealed class Tema
public sealed class Tema : ITValuePublisher
{
private struct EmaState
{
@@ -49,8 +49,9 @@ public sealed class Tema
private double _lastValidValue;
public string Name { get; }
public TValue Value { get; private set; }
public TValue Last { get; private set; }
public bool IsHot => _state3.E <= 0.09;
public event Action<TValue>? Pub;
public Tema(int period)
{
@@ -61,6 +62,11 @@ public sealed class Tema
Name = $"Tema({period})";
}
public Tema(ITValuePublisher source, int period) : this(period)
{
source.Pub += (item) => Update(item);
}
public Tema(double alpha)
{
if (alpha <= 0 || alpha > 1) throw new ArgumentException("Alpha must be between 0 and 1", nameof(alpha));
@@ -102,8 +108,9 @@ public sealed class Tema
double e3 = Compute(e2, _alpha, _decay, ref _state3);
double result = 3 * e1 - 3 * e2 + e3;
Value = new TValue(input.Time, result);
return Value;
Last = new TValue(input.Time, result);
Pub?.Invoke(Last);
return Last;
}
public TSeries Update(TSeries source)
@@ -154,7 +161,7 @@ public sealed class Tema
_p_state3 = s3;
_lastValidValue = lastValid;
Value = new TValue(tSpan[len - 1], vSpan[len - 1]);
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
return new TSeries(t, v);
}
@@ -322,6 +329,6 @@ public sealed class Tema
_p_state2 = EmaState.New();
_p_state3 = EmaState.New();
_lastValidValue = 0;
Value = default;
Last = default;
}
}