Files
2026-02-10 21:33:16 -08:00

492 lines
14 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// TEMA: Triple Exponential Moving Average
/// </summary>
/// <remarks>
/// Uses triple smoothing to further reduce lag beyond DEMA.
/// Excellent for fast trend identification with minimal overshoot.
///
/// Calculation: <c>TEMA = 3×EMA1 - 3×EMA2 + EMA3</c> (cascaded EMAs).
/// </remarks>
/// <seealso href="Tema.md">Detailed documentation</seealso>
/// <seealso href="tema.pine">Reference Pine Script implementation</seealso>
[SkipLocalsInit]
public sealed class Tema : AbstractBase
{
[StructLayout(LayoutKind.Auto)]
private record struct EmaState(double Ema, double E, bool IsHot, bool IsCompensated)
{
public static EmaState New() => new() { Ema = 0, E = 1.0, IsHot = false, IsCompensated = false };
}
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 readonly TValuePublishedHandler _handler;
private double _lastValidValue;
private double _p_lastValidValue;
public override bool IsHot => _state3.E <= 0.09;
public Tema(int period)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
_alpha = 2.0 / (period + 1);
_decay = 1.0 - _alpha;
Name = $"Tema({period})";
WarmupPeriod = period * 3;
_handler = Handle;
}
public Tema(ITValuePublisher source, int period) : this(period)
{
source.Pub += _handler;
}
public Tema(TSeries source, int period) : this(period)
{
Prime(source.Values);
if (source.Count > 0)
{
Last = new TValue(source.LastTime, Last.Value);
}
source.Pub += _handler;
}
public Tema(double alpha)
{
if (alpha <= 0 || alpha >= 1)
{
throw new ArgumentException("Alpha must be strictly between 0 and 1", nameof(alpha));
}
_alpha = alpha;
_decay = 1.0 - alpha;
Name = $"Tema(α={alpha:F4})";
WarmupPeriod = (int)(3 * (2.0 / alpha - 1.0));
_handler = Handle;
}
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
/// <summary>
/// Initializes the indicator state using the provided history.
/// </summary>
/// <param name="source">Historical data</param>
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
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;
// Search for the first finite value to initialize lastValid
// If no finite value is found, lastValid remains 0
for (int i = 0; i < len; i++)
{
if (double.IsFinite(source[i]))
{
lastValid = source[i];
break;
}
}
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);
// TEMA = 3 * e1 - 3 * e2 + e3 = FMA(3, e1, FMA(-3, e2, e3))
double result = Math.FusedMultiplyAdd(3.0, e1_final, Math.FusedMultiplyAdd(-3.0, 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 override TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
_p_state1 = _state1;
_p_state2 = _state2;
_p_state3 = _state3;
_p_lastValidValue = _lastValidValue;
}
else
{
_state1 = _p_state1;
_state2 = _p_state2;
_state3 = _p_state3;
_lastValidValue = _p_lastValidValue;
}
// EMA1
double val = input.Value;
if (double.IsFinite(val))
{
_lastValidValue = val;
}
else
{
val = _lastValidValue;
}
double e1 = Compute(val, _alpha, _decay, ref _state1);
// EMA2 (input is e1)
double e2 = Compute(e1, _alpha, _decay, ref _state2);
// EMA3 (input is e2)
double e3 = Compute(e2, _alpha, _decay, ref _state3);
// TEMA = 3 * e1 - 3 * e2 + e3 = FMA(3, e1, FMA(-3, e2, e3))
double result = Math.FusedMultiplyAdd(3.0, e1, Math.FusedMultiplyAdd(-3.0, e2, e3));
Last = new TValue(input.Time, result);
PubEvent(Last, isNew);
return Last;
}
public override TSeries Update(TSeries source)
{
if (source.Count == 0)
{
return [];
}
int len = source.Count;
List<long> t = new(len);
List<double> v = new(len);
CollectionsMarshal.SetCount(t, len);
CollectionsMarshal.SetCount(v, len);
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
source.Times.CopyTo(tSpan);
var sourceValues = source.Values;
// Use current state
EmaState s1 = _state1;
EmaState s2 = _state2;
EmaState s3 = _state3;
double lastValid = _lastValidValue;
double alpha = _alpha;
double decay = _decay;
for (int i = 0; i < len; i++)
{
double val = sourceValues[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);
double e3 = Compute(e2, alpha, decay, ref s3);
// TEMA = 3 * e1 - 3 * e2 + e3 = FMA(3, e1, FMA(-3, e2, e3))
vSpan[i] = Math.FusedMultiplyAdd(3.0, e1, Math.FusedMultiplyAdd(-3.0, e2, e3));
}
// Update instance state
_state1 = s1;
_state2 = s2;
_state3 = s3;
_p_state1 = s1;
_p_state2 = s2;
_p_state3 = s3;
_lastValidValue = lastValid;
_p_lastValidValue = lastValid;
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
return new TSeries(t, v);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double Compute(double input, double alpha, double decay, ref EmaState state)
{
// EMA update: ema = decay * ema + alpha * input = FMA(decay, ema, alpha * input)
state.Ema = Math.FusedMultiplyAdd(decay, state.Ema, alpha * input);
double result;
if (!state.IsCompensated)
{
state.E *= decay;
if (!state.IsHot && state.E <= 0.05) // COVERAGE_THRESHOLD
{
state.IsHot = true;
}
if (state.E <= 1e-10) // COMPENSATOR_THRESHOLD
{
state.IsCompensated = true;
result = state.Ema;
}
else
{
result = state.Ema / (1.0 - state.E);
}
}
else
{
result = state.Ema;
}
return result;
}
public static TSeries Batch(TSeries source, int period)
{
var tema = new Tema(period);
return tema.Update(source);
}
public static TSeries Batch(TSeries source, double alpha)
{
var tema = new Tema(alpha);
return tema.Update(source);
}
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);
Batch(source, output, 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", nameof(output));
}
if (alpha <= 0 || alpha >= 1)
{
throw new ArgumentException("Alpha must be strictly between 0 and 1", nameof(alpha));
}
if (source.Length == 0)
{
return;
}
double decay = 1.0 - alpha;
double lastValid = 0;
// Search for the first finite value to initialize lastValid
for (int i = 0; i < source.Length; i++)
{
if (double.IsFinite(source[i]))
{
lastValid = source[i];
break;
}
}
// State for EMA1
double ema1_val = 0;
double ema1_e = 1.0;
bool ema1_isCompensated = false;
// State for EMA2
double ema2_val = 0;
double ema2_e = 1.0;
bool ema2_isCompensated = false;
// State for EMA3
double ema3_val = 0;
double ema3_e = 1.0;
bool ema3_isCompensated = false;
for (int i = 0; i < source.Length; i++)
{
double val = source[i];
if (double.IsFinite(val))
{
lastValid = val;
}
else
{
val = lastValid;
}
// Update EMA1: ema = decay * ema + alpha * input = FMA(decay, ema, alpha * input)
ema1_val = Math.FusedMultiplyAdd(decay, ema1_val, alpha * val);
double e1;
if (!ema1_isCompensated)
{
ema1_e *= decay;
if (ema1_e <= 1e-10)
{
ema1_isCompensated = true;
e1 = ema1_val;
}
else
{
e1 = ema1_val / (1.0 - ema1_e);
}
}
else
{
e1 = ema1_val;
}
// Update EMA2 (input is e1): ema = decay * ema + alpha * input
ema2_val = Math.FusedMultiplyAdd(decay, ema2_val, alpha * e1);
double e2;
if (!ema2_isCompensated)
{
ema2_e *= decay;
if (ema2_e <= 1e-10)
{
ema2_isCompensated = true;
e2 = ema2_val;
}
else
{
e2 = ema2_val / (1.0 - ema2_e);
}
}
else
{
e2 = ema2_val;
}
// Update EMA3 (input is e2): ema = decay * ema + alpha * input
ema3_val = Math.FusedMultiplyAdd(decay, ema3_val, alpha * e2);
double e3;
if (!ema3_isCompensated)
{
ema3_e *= decay;
if (ema3_e <= 1e-10)
{
ema3_isCompensated = true;
e3 = ema3_val;
}
else
{
e3 = ema3_val / (1.0 - ema3_e);
}
}
else
{
e3 = ema3_val;
}
// TEMA = 3 * EMA1 - 3 * EMA2 + EMA3 = FMA(3, e1, FMA(-3, e2, e3))
output[i] = Math.FusedMultiplyAdd(3.0, e1, Math.FusedMultiplyAdd(-3.0, e2, e3));
}
}
public static (TSeries Results, Tema Indicator) Calculate(TSeries source, int period)
{
var indicator = new Tema(period);
TSeries results = indicator.Update(source);
return (results, indicator);
}
public override void Reset()
{
_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;
Last = default;
}
}