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
+46 -4
View File
@@ -19,12 +19,12 @@ public class TrimaTests
{
var trima = new Trima(10);
Assert.Equal(0, trima.Value.Value);
Assert.Equal(0, trima.Last.Value);
TValue result = trima.Update(new TValue(DateTime.UtcNow, 100));
Assert.True(result.Value > 0);
Assert.Equal(result.Value, trima.Value.Value);
Assert.Equal(result.Value, trima.Last.Value);
}
[Fact]
@@ -111,12 +111,12 @@ public class TrimaTests
trima.Reset();
Assert.Equal(0, trima.Value.Value);
Assert.Equal(0, trima.Last.Value);
Assert.False(trima.IsHot);
// After reset, should accept new values
trima.Update(new TValue(DateTime.UtcNow, 50));
Assert.NotEqual(0, trima.Value.Value);
Assert.NotEqual(0, trima.Last.Value);
}
[Fact]
@@ -197,4 +197,46 @@ public class TrimaTests
Assert.Equal(tseriesResult[i].Value, output[i], 1e-10);
}
}
[Fact]
public void Trima_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 = Trima.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];
Trima.Calculate(spanInput, spanOutput, period);
double spanResult = spanOutput[^1];
// 3. Streaming Mode
var streamingInd = new Trima(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 Trima(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);
}
}
+14 -7
View File
@@ -23,7 +23,7 @@ namespace QuanTAlib;
/// Becomes true when the buffer is full (period samples processed).
/// </remarks>
[SkipLocalsInit]
public sealed class Trima
public sealed class Trima : ITValuePublisher
{
private readonly int _period;
private readonly int _p1;
@@ -41,8 +41,9 @@ public sealed class Trima
private const int ResyncInterval = 1000;
public string Name { get; }
public TValue Value { get; private set; }
public TValue Last { get; private set; }
public bool IsHot => _sampleCount >= _period;
public event Action<TValue>? Pub;
public Trima(int period)
{
@@ -58,6 +59,11 @@ public sealed class Trima
Name = $"Trima({period})";
}
public Trima(ITValuePublisher source, int period) : this(period)
{
source.Pub += (item) => Update(item);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double GetValidValue(double input)
{
@@ -110,7 +116,7 @@ public sealed class Trima
_p_sum2 = _sum2;
_p_lastInput2 = sma1Result;
Value = new TValue(input.Time, _sum2 / _buffer2.Count);
Last = new TValue(input.Time, _sum2 / _buffer2.Count);
}
else
{
@@ -126,10 +132,11 @@ public sealed class Trima
_sum2 = _p_sum2 - _p_lastInput2 + sma1Result;
_buffer2.UpdateNewest(sma1Result);
Value = new TValue(input.Time, _sum2 / _buffer2.Count);
Last = new TValue(input.Time, _sum2 / _buffer2.Count);
}
return Value;
Pub?.Invoke(Last);
return Last;
}
public TSeries Update(TSeries source)
@@ -158,7 +165,7 @@ public sealed class Trima
Update(new TValue(source.Times[i], source.Values[i]), isNew: true);
}
Value = new TValue(tSpan[len - 1], vSpan[len - 1]);
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
return new TSeries(t, v);
}
@@ -204,6 +211,6 @@ public sealed class Trima
_tickCount2 = 0;
_sampleCount = 0;
Value = default;
Last = default;
}
}