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
+1 -1
View File
@@ -116,7 +116,7 @@ public class TemaIndicatorTests
{
var indicator = new TemaIndicator();
indicator.Initialize();
var method = indicator.GetType().GetMethod("OnPaintChart");
Assert.NotNull(method);
Assert.Equal(typeof(TemaIndicator), method.DeclaringType);
+8 -8
View File
@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using Xunit;
namespace QuanTAlib;
namespace QuanTAlib.Tests;
public class TemaTests
{
@@ -101,7 +101,7 @@ public class TemaTests
}
[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 TemaTests
streamingResults.Add(tema.Update(series[i]).Value);
}
var staticResults = Tema.Calculate(series, 10);
var batchResults = Tema.Batch(series, 10);
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 TemaTests
}
var spanResults = new double[series.Count];
Tema.Calculate(series.Values, spanResults, 10);
Tema.Batch(series.Values, spanResults, 10);
for (int i = 0; i < spanResults.Length; i++)
{
+1 -1
View File
@@ -112,7 +112,7 @@ public class TemaValidationTests
{
// Calculate QuanTAlib TEMA (Span API)
double[] qOutput = new double[_testData.RawData.Length];
global::QuanTAlib.Tema.Calculate(_testData.RawData.Span, qOutput.AsSpan(), period);
global::QuanTAlib.Tema.Batch(_testData.RawData.Span, qOutput.AsSpan(), period);
// Calculate TA-Lib TEMA
var retCode = TALib.Functions.Tema<double>(_testData.RawData.Span, 0..^0, talibOutput, out var outRange, period);
+101 -17
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 : ITValuePublisher
public sealed class Tema : AbstractBase
{
private record struct EmaState(double Ema, double E, bool IsHot, bool IsCompensated)
{
@@ -33,21 +33,18 @@ public sealed class Tema : ITValuePublisher
private readonly double _alpha;
private readonly double _decay;
private EmaState _state1 = EmaState.New();
private EmaState _state2 = EmaState.New();
private EmaState _state3 = EmaState.New();
private EmaState _p_state1 = EmaState.New();
private EmaState _p_state2 = EmaState.New();
private EmaState _p_state3 = EmaState.New();
private double _lastValidValue;
private double _p_lastValidValue;
public string Name { get; }
public TValue Last { get; private set; }
public bool IsHot => _state3.E <= 0.09;
public event Action<TValue>? Pub;
public override bool IsHot => _state3.E <= 0.09;
public Tema(int period)
{
@@ -56,6 +53,7 @@ public sealed class Tema : ITValuePublisher
_alpha = 2.0 / (period + 1);
_decay = 1.0 - _alpha;
Name = $"Tema({period})";
WarmupPeriod = period * 3;
}
public Tema(ITValuePublisher source, int period) : this(period)
@@ -63,6 +61,16 @@ public sealed class Tema : ITValuePublisher
source.Pub += (item) => Update(item);
}
public Tema(TSeries source, int period) : this(period)
{
Prime(source.Values);
if (source.Count > 0)
{
Last = new TValue(source.LastTime, Last.Value);
}
source.Pub += (item) => Update(item);
}
public Tema(double alpha)
{
if (alpha <= 0 || alpha >= 1) throw new ArgumentException("Alpha must be strictly between 0 and 1", nameof(alpha));
@@ -70,10 +78,86 @@ public sealed class Tema : ITValuePublisher
_alpha = alpha;
_decay = 1.0 - alpha;
Name = $"Tema(α={alpha:F4})";
WarmupPeriod = (int)(3 * (2.0 / alpha - 1.0));
}
/// <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
_state1 = EmaState.New();
_state2 = EmaState.New();
_state3 = EmaState.New();
_p_state1 = EmaState.New();
_p_state2 = EmaState.New();
_p_state3 = EmaState.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 lastValid = 0;
EmaState s1 = _state1;
EmaState s2 = _state2;
EmaState s3 = _state3;
double alpha = _alpha;
double decay = _decay;
for (int i = 0; i < len; i++)
{
double val = source[i];
if (double.IsFinite(val))
lastValid = val;
else
val = lastValid;
double e1 = Compute(val, alpha, decay, ref s1);
double e2 = Compute(e1, alpha, decay, ref s2);
Compute(e2, alpha, decay, ref s3);
}
_state1 = s1;
_state2 = s2;
_state3 = s3;
_lastValidValue = lastValid;
// Calculate the initial "Last" value
// We need to re-compute the last step to get the result
// But Compute updates state, so we can't just call it again without side effects if we pass ref state.
// However, we can calculate the result from the current state.
// TEMA = 3 * EMA1 - 3 * EMA2 + EMA3
// The state contains the updated EMA values (Ema field).
// But wait, Compute returns the *compensated* value.
// The state.Ema is the raw EMA value.
// We need to apply compensation logic to get the correct E1, E2, E3.
double GetCompensated(EmaState s)
{
if (s.IsCompensated) return s.Ema;
return s.Ema / (1.0 - s.E);
}
double e1_final = GetCompensated(_state1);
double e2_final = GetCompensated(_state2);
double e3_final = GetCompensated(_state3);
double result = 3 * e1_final - 3 * e2_final + e3_final;
Last = new TValue(DateTime.MinValue, result);
_p_state1 = _state1;
_p_state2 = _state2;
_p_state3 = _state3;
_p_lastValidValue = _lastValidValue;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
public override TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
@@ -107,11 +191,11 @@ public sealed class Tema : ITValuePublisher
double result = 3 * e1 - 3 * e2 + e3;
Last = new TValue(input.Time, result);
Pub?.Invoke(Last);
PubEvent(Last);
return Last;
}
public TSeries Update(TSeries source)
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
@@ -126,7 +210,7 @@ public sealed class Tema : ITValuePublisher
source.Times.CopyTo(tSpan);
var sourceValues = source.Values;
// Use current state
EmaState s1 = _state1;
EmaState s2 = _state2;
@@ -195,28 +279,28 @@ public sealed class Tema : ITValuePublisher
return result;
}
public static TSeries Calculate(TSeries source, int period)
public static TSeries Batch(TSeries source, int period)
{
var tema = new Tema(period);
return tema.Update(source);
}
public static TSeries Calculate(TSeries source, double alpha)
public static TSeries Batch(TSeries source, double alpha)
{
var tema = new Tema(alpha);
return tema.Update(source);
}
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period)
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period)
{
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
double alpha = 2.0 / (period + 1);
Calculate(source, output, alpha);
Batch(source, output, alpha);
}
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, double alpha)
public static void Batch(ReadOnlySpan<double> source, Span<double> output, double alpha)
{
if (source.Length != output.Length)
throw new ArgumentException("Source and output must have the same length");
@@ -319,7 +403,7 @@ public sealed class Tema : ITValuePublisher
}
}
public void Reset()
public override void Reset()
{
_state1 = EmaState.New();
_state2 = EmaState.New();
+3 -3
View File
@@ -61,12 +61,12 @@ Console.WriteLine($"Current TEMA: {result.Value}");
// Batch calculation (TSeries API)
TSeries source = ...;
TSeries results = Tema.Calculate(source, 14);
TSeries results = Tema.Batch(source, 14);
// High-performance Span API (zero allocation)
double[] prices = new double[10000];
double[] output = new double[10000];
Tema.Calculate(prices.AsSpan(), output.AsSpan(), period: 14);
Tema.Batch(prices.AsSpan(), output.AsSpan(), period: 14);
```
### Zero-Allocation Span API
@@ -79,7 +79,7 @@ double[] source = new double[200000];
double[] temaOutput = new double[200000];
// Zero heap allocation during calculation
Tema.Calculate(source.AsSpan(), temaOutput.AsSpan(), period: 50);
Tema.Batch(source.AsSpan(), temaOutput.AsSpan(), period: 50);
```
### Eventing and Reactive Support