refactoring

This commit is contained in:
Miha Kralj
2025-12-16 21:16:50 -08:00
parent a67ad65fa5
commit d277e08056
137 changed files with 5074 additions and 3178 deletions
+8 -8
View File
@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using Xunit;
namespace QuanTAlib;
namespace QuanTAlib.Tests;
public class T3Tests
{
@@ -101,7 +101,7 @@ public class T3Tests
}
[Fact]
public void StaticCalculate_Matches_Streaming()
public void BatchCalculate_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
@@ -114,17 +114,17 @@ public class T3Tests
streamingResults.Add(t3.Update(series[i]).Value);
}
var staticResults = T3.Calculate(series, 5, 0.7);
var batchResults = T3.Batch(series, 5, 0.7);
Assert.Equal(streamingResults.Count, staticResults.Count);
for (int i = 0; i < staticResults.Count; i++)
Assert.Equal(streamingResults.Count, batchResults.Count);
for (int i = 0; i < batchResults.Count; i++)
{
Assert.Equal(streamingResults[i], staticResults.Values[i], 1e-9);
Assert.Equal(streamingResults[i], batchResults.Values[i], 1e-9);
}
}
[Fact]
public void StaticCalculateSpan_Matches_Streaming()
public void BatchCalculateSpan_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
@@ -138,7 +138,7 @@ public class T3Tests
}
var spanResults = new double[series.Count];
T3.Calculate(series.Values, spanResults, 5, 0.7);
T3.Batch(series.Values, spanResults, 5, 0.7);
for (int i = 0; i < spanResults.Length; i++)
{
+1 -1
View File
@@ -114,7 +114,7 @@ public class T3ValidationTests
{
// Calculate QuanTAlib T3 (Span API)
double[] qOutput = new double[_testData.RawData.Length];
global::QuanTAlib.T3.Calculate(_testData.RawData.Span, qOutput.AsSpan(), period, vFactor);
global::QuanTAlib.T3.Batch(_testData.RawData.Span, qOutput.AsSpan(), period, vFactor);
// Calculate TA-Lib T3
var retCode = TALib.Functions.T3<double>(_testData.RawData.Span, 0..^0, talibOutput, out var outRange, period, vFactor);
+81 -17
View File
@@ -24,7 +24,7 @@ namespace QuanTAlib;
/// alpha = 2 / (period + 1)
/// </remarks>
[SkipLocalsInit]
public sealed class T3 : ITValuePublisher
public sealed class T3 : AbstractBase
{
private record struct State(double E1, double E2, double E3, double E4, double E5, double E6, bool IsInitialized)
{
@@ -67,13 +67,6 @@ public sealed class T3 : ITValuePublisher
private double _lastValidValue;
private double _p_lastValidValue;
/// <summary>
/// Display name for the indicator.
/// </summary>
public string Name { get; }
public event Action<TValue>? Pub;
/// <summary>
/// Creates T3 with specified period and volume factor.
/// </summary>
@@ -99,6 +92,7 @@ public sealed class T3 : ITValuePublisher
_params = new Parameters(alpha, c1, c2, c3, c4);
Name = $"T3({period}, {vfactor:F2})";
WarmupPeriod = period * 6; // T3 has 6 cascaded EMAs, so warmup is longer
}
/// <summary>
@@ -114,14 +108,84 @@ public sealed class T3 : ITValuePublisher
}
/// <summary>
/// Current T3 value.
/// Creates T3 with specified source, period and volume factor.
/// </summary>
public TValue Last { get; private set; }
/// <param name="source">Source series</param>
/// <param name="period">Period for EMA calculation</param>
/// <param name="vfactor">Volume Factor (default 0.7)</param>
public T3(TSeries source, int period, double vfactor = 0.7) : this(period, vfactor)
{
Prime(source.Values);
if (source.Count > 0)
{
Last = new TValue(source.LastTime, Last.Value);
}
source.Pub += (item) => Update(item);
}
/// <summary>
/// True if the T3 has been initialized (received at least one value).
/// </summary>
public bool IsHot => _state.IsInitialized;
public override bool IsHot => _state.IsInitialized;
/// <summary>
/// Initializes the indicator state using the provided history.
/// </summary>
/// <param name="source">Historical data</param>
public override void Prime(ReadOnlySpan<double> source)
{
if (source.Length == 0) return;
// Reset state
_state = State.New();
_p_state = State.New();
_lastValidValue = 0;
_p_lastValidValue = 0;
// Run the calculation on the history to update state
// We don't need the output, just the final state
int len = source.Length;
double lastValidValue = 0;
State state = _state;
for (int i = 0; i < len; i++)
{
double val = source[i];
if (double.IsFinite(val))
lastValidValue = val;
else
val = lastValidValue;
Compute(val, _params, ref state);
}
_state = state;
_lastValidValue = lastValidValue;
// Calculate the initial "Last" value
// We need to re-compute the last step to get the result, or just use the state if we stored the result
// Since Compute returns the result but also updates state, we can't easily get the last result without re-running or storing it.
// However, Prime is usually followed by Update or we just need the state ready.
// If we want Last to be correct, we should probably store the last result.
// But AbstractBase.Prime doesn't strictly require Last to be set to the very last value of source,
// though it's good practice.
// Let's re-run the last value computation to set Last correctly.
if (len > 0)
{
// We need to be careful not to double-apply the last update if we just loop.
// Actually, the loop above updated the state to include the last value.
// So the state corresponds to "after processing source".
// To get the output value corresponding to the last input, we can calculate it from the state.
// But T3 formula uses the *updated* EMAs.
// T3 = c1*e6 + c2*e5 + c3*e4 + c4*e3
// The state has the updated EMAs.
double result = _params.C1 * _state.E6 + _params.C2 * _state.E5 + _params.C3 * _state.E4 + _params.C4 * _state.E3;
Last = new TValue(DateTime.MinValue, result);
}
_p_state = _state;
_p_lastValidValue = _lastValidValue;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double GetValidValue(double input)
@@ -135,7 +199,7 @@ public sealed class T3 : ITValuePublisher
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
public override TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
@@ -151,11 +215,11 @@ public sealed class T3 : ITValuePublisher
double val = GetValidValue(input.Value);
val = Compute(val, _params, ref _state);
Last = new TValue(input.Time, val);
Pub?.Invoke(Last);
PubEvent(Last);
return Last;
}
public TSeries Update(TSeries source)
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
@@ -227,7 +291,7 @@ public sealed class T3 : ITValuePublisher
/// <summary>
/// Calculates T3 for the entire series using a new instance.
/// </summary>
public static TSeries Calculate(TSeries source, int period, double vfactor = 0.7)
public static TSeries Batch(TSeries source, int period, double vfactor = 0.7)
{
var t3 = new T3(period, vfactor);
return t3.Update(source);
@@ -237,7 +301,7 @@ public sealed class T3 : ITValuePublisher
/// Calculates T3 in-place using period, writing results to pre-allocated output span.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period, double vfactor = 0.7)
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period, double vfactor = 0.7)
{
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
@@ -264,7 +328,7 @@ public sealed class T3 : ITValuePublisher
/// <summary>
/// Resets the T3 state.
/// </summary>
public void Reset()
public override void Reset()
{
_state = State.New();
_p_state = _state;
+2 -2
View File
@@ -45,10 +45,10 @@ Where:
```csharp
// Calculate T3 with period 10 and default volume factor 0.7
var t3 = T3.Calculate(sourceSeries, 10);
var t3 = T3.Batch(sourceSeries, 10);
// Calculate T3 with period 10 and volume factor 0.618
var t3_custom = T3.Calculate(sourceSeries, 10, 0.618);
var t3_custom = T3.Batch(sourceSeries, 10, 0.618);
Console.WriteLine($"T3 Value: {t3.Last.Value}");
```