SIMD Refactor: Merge simd-dev into dev (#55)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4) <aider@aider.chat>
Co-authored-by: Warp <agent@warp.dev>
This commit is contained in:
Miha Kralj
2026-01-18 19:02:03 -08:00
committed by GitHub
co-authored by Claude Opus 4.5 aider Warp
parent 5bcdf8d614
commit 86fe32a682
1750 changed files with 198235 additions and 80539 deletions
+171
View File
@@ -0,0 +1,171 @@
using TradingPlatform.BusinessLayer;
namespace QuanTAlib.Tests;
public class TemaIndicatorTests
{
[Fact]
public void TemaIndicator_Constructor_SetsDefaults()
{
var indicator = new TemaIndicator();
Assert.Equal(10, indicator.Period);
Assert.Equal(SourceType.Close, indicator.Source);
Assert.True(indicator.ShowColdValues);
Assert.Equal("TEMA - Triple Exponential Moving Average", indicator.Name);
Assert.False(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void TemaIndicator_MinHistoryDepths_EqualsPeriod()
{
var indicator = new TemaIndicator { Period = 20 };
Assert.Equal(0, TemaIndicator.MinHistoryDepths);
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
[Fact]
public void TemaIndicator_ShortName_IncludesPeriodAndSource()
{
var indicator = new TemaIndicator { Period = 15 };
Assert.Contains("TEMA", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("15", indicator.ShortName, StringComparison.Ordinal);
}
[Fact]
public void TemaIndicator_SourceCodeLink_IsValid()
{
var indicator = new TemaIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
Assert.Contains("Tema.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
}
[Fact]
public void TemaIndicator_Initialize_CreatesInternalTema()
{
var indicator = new TemaIndicator { Period = 10 };
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void TemaIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new TemaIndicator { Period = 3 };
indicator.Initialize();
// Add historical data
var now = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 102);
// Process update
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
}
// Line series should have a value
Assert.True(indicator.LinesSeries[0].Count > 0);
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
}
[Fact]
public void TemaIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new TemaIndicator { Period = 3 };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
[Fact]
public void TemaIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
{
var indicator = new TemaIndicator { Period = 3 };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 50; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 102);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double firstValue = indicator.LinesSeries[0].GetValue(0);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
double secondValue = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(firstValue));
Assert.True(double.IsFinite(secondValue));
}
[Fact]
public void TemaIndicator_MultipleUpdates_ProducesCorrectTemaSequence()
{
var indicator = new TemaIndicator { Period = 3 };
indicator.Initialize();
var now = DateTime.UtcNow;
double[] closes = { 100, 102, 104, 103, 105 };
foreach (var close in closes)
{
indicator.HistoricalData.AddBar(now, close, close + 2, close - 2, close);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
now = now.AddMinutes(1);
}
// All values should be finite
for (int i = 0; i < closes.Length; i++)
{
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(closes.Length - 1 - i)));
}
}
[Fact]
public void TemaIndicator_DifferentSourceTypes_Work()
{
var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 };
foreach (var source in sources)
{
var indicator = new TemaIndicator { Period = 3, Source = source };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)),
$"Source {source} should produce finite value");
}
}
[Fact]
public void TemaIndicator_Period_CanBeChanged()
{
var indicator = new TemaIndicator { Period = 5 };
Assert.Equal(5, indicator.Period);
indicator.Period = 20;
Assert.Equal(20, indicator.Period);
Assert.Equal(0, TemaIndicator.MinHistoryDepths);
}
}
+62
View File
@@ -0,0 +1,62 @@
using System.Drawing;
using System.Runtime.CompilerServices;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
[SkipLocalsInit]
public sealed class TemaIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Period", sortIndex: 1, 1, 1000, 1, 0)]
public int Period { get; set; } = 10;
[IndicatorExtensions.DataSourceInput]
public SourceType Source { get; set; } = SourceType.Close;
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private Tema _ma = null!;
private readonly LineSeries _series;
private string _sourceName = null!;
private Func<IHistoryItem, double> _priceSelector = null!;
public static int MinHistoryDepths => 0;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => $"TEMA {Period}:{_sourceName}";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/trends/tema/Tema.Quantower.cs";
public TemaIndicator()
{
OnBackGround = true;
SeparateWindow = false;
_sourceName = Source.ToString();
Name = "TEMA - Triple Exponential Moving Average";
Description = "Triple Exponential Moving Average";
_series = new LineSeries(name: $"TEMA {Period}", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
AddLineSeries(_series);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnInit()
{
_ma = new Tema(Period);
_sourceName = Source.ToString();
_priceSelector = Source.GetPriceSelector();
base.OnInit();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnUpdate(UpdateArgs args)
{
if (args.Reason != UpdateReason.NewBar && args.Reason != UpdateReason.HistoricalBar && args.Reason != UpdateReason.NewTick)
return;
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
TValue result = _ma.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), args.IsNewBar());
_series.SetValue(result.Value, _ma.IsHot, ShowColdValues);
_series.SetMarker(0, Color.Transparent);
}
}
+172
View File
@@ -0,0 +1,172 @@
namespace QuanTAlib.Tests;
public class TemaTests
{
[Fact]
public void BasicCalculation_DoesNotCrash()
{
var tema = new Tema(10);
var gbm = new GBM();
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < bars.Count; i++)
{
tema.Update(new TValue(bars[i].Time, bars[i].Close));
}
Assert.True(double.IsFinite(tema.Last.Value));
}
[Fact]
public void IsNew_Consistency()
{
var tema = new Tema(10);
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Feed first 99
for (int i = 0; i < 99; i++)
{
tema.Update(new TValue(bars[i].Time, bars[i].Close));
}
// Update with 100th point (isNew=true)
tema.Update(new TValue(bars[99].Time, bars[99].Close), true);
// Update with modified 100th point (isNew=false)
var val2 = tema.Update(new TValue(bars[99].Time, bars[99].Close + 1.0), false);
// Create new instance and feed up to modified
var tema2 = new Tema(10);
for (int i = 0; i < 99; i++)
{
tema2.Update(new TValue(bars[i].Time, bars[i].Close));
}
var val3 = tema2.Update(new TValue(bars[99].Time, bars[99].Close + 1.0), true);
Assert.Equal(val3.Value, val2.Value, 1e-9);
}
[Fact]
public void Reset_Works()
{
var tema = new Tema(10);
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < bars.Count; i++)
{
tema.Update(new TValue(bars[i].Time, bars[i].Close));
}
tema.Reset();
Assert.Equal(0, tema.Last.Value);
Assert.False(tema.IsHot);
// Feed again
for (int i = 0; i < bars.Count; i++)
{
tema.Update(new TValue(bars[i].Time, bars[i].Close));
}
Assert.True(double.IsFinite(tema.Last.Value));
}
[Fact]
public void TSeries_Update_Matches_Streaming()
{
var tema = new Tema(10);
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamingResults.Add(tema.Update(series[i]).Value);
}
var tema2 = new Tema(10);
var seriesResults = tema2.Update(series);
Assert.Equal(streamingResults.Count, seriesResults.Count);
for (int i = 0; i < seriesResults.Count; i++)
{
Assert.Equal(streamingResults[i], seriesResults.Values[i], 1e-9);
}
}
[Fact]
public void BatchCalculate_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var tema = new Tema(10);
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamingResults.Add(tema.Update(series[i]).Value);
}
var batchResults = Tema.Batch(series, 10);
Assert.Equal(streamingResults.Count, batchResults.Count);
for (int i = 0; i < batchResults.Count; i++)
{
Assert.Equal(streamingResults[i], batchResults.Values[i], 1e-9);
}
}
[Fact]
public void BatchCalculateSpan_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var tema = new Tema(10);
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamingResults.Add(tema.Update(series[i]).Value);
}
var spanResults = new double[series.Count];
Tema.Batch(series.Values, spanResults, 10);
for (int i = 0; i < spanResults.Length; i++)
{
Assert.Equal(streamingResults[i], spanResults[i], 1e-9);
}
}
[Fact]
public void Chainability_Works()
{
var tema = new Tema(10);
var gbm = new GBM();
var bars = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
// Test TSeries chain
var result = tema.Update(series);
Assert.NotNull(result);
Assert.IsType<TSeries>(result);
// Test TValue chain
var result2 = tema.Update(series[0]);
Assert.IsType<TValue>(result2);
}
[Fact]
public void Constructor_InvalidParameters_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new Tema(0));
Assert.Throws<ArgumentException>(() => new Tema(-1));
Assert.Throws<ArgumentException>(() => new Tema(0.0));
Assert.Throws<ArgumentException>(() => new Tema(1.0));
}
}
@@ -0,0 +1,122 @@
using Skender.Stock.Indicators;
using TALib;
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
public class TemaValidationTests
{
// Note: OoplesFinance TEMA implementation diverges significantly from Skender, TA-Lib, and Tulip
// for larger periods, likely due to different initialization or smoothing logic.
// Therefore, we do not validate against Ooples for TEMA.
private readonly ValidationTestData _testData;
private readonly ITestOutputHelper _output;
public TemaValidationTests(ITestOutputHelper output)
{
_output = output;
_testData = new ValidationTestData();
}
[Fact]
public void Validate_Skender_Batch()
{
int[] periods = { 5, 10, 20, 50, 100 };
foreach (var period in periods)
{
// Calculate QuanTAlib TEMA (batch TSeries)
var tema = new global::QuanTAlib.Tema(period);
var qResult = tema.Update(_testData.Data);
// Calculate Skender TEMA
var sResult = _testData.SkenderQuotes.GetTema(period).ToList();
// Compare last 100 records
ValidationHelper.VerifyData(qResult, sResult, x => x.Tema, tolerance: ValidationHelper.SkenderTolerance);
}
_output.WriteLine("TEMA Batch(TSeries) validated successfully against Skender.Stock.Indicators");
}
[Fact]
public void Validate_Talib_Batch()
{
int[] periods = { 5, 10, 20, 50, 100 };
// Prepare data for TA-Lib (double[])
double[] output = new double[_testData.RawData.Length];
foreach (var period in periods)
{
// Calculate QuanTAlib TEMA (batch TSeries)
var tema = new global::QuanTAlib.Tema(period);
var qResult = tema.Update(_testData.Data);
// Calculate TA-Lib TEMA
var retCode = TALib.Functions.Tema<double>(_testData.RawData.Span, 0..^0, output, out var outRange, period);
Assert.Equal(Core.RetCode.Success, retCode);
int lookback = TALib.Functions.TemaLookback(period);
// Compare last 100 records
ValidationHelper.VerifyData(qResult, output, outRange, lookback, tolerance: ValidationHelper.TalibTolerance);
}
_output.WriteLine("TEMA Batch(TSeries) validated successfully against TA-Lib");
}
[Fact]
public void Validate_Tulip_Batch()
{
int[] periods = { 5, 10, 20, 50, 100 };
foreach (var period in periods)
{
// Calculate QuanTAlib TEMA (batch TSeries)
var tema = new global::QuanTAlib.Tema(period);
var qResult = tema.Update(_testData.Data);
// Calculate Tulip TEMA
var temaIndicator = Tulip.Indicators.tema;
double[][] inputs = { _testData.RawData.ToArray() };
double[] options = { period };
// Tulip TEMA lookback is 3*(period-1)
int lookback = 3 * (period - 1);
double[][] outputs = { new double[_testData.RawData.Length - lookback] };
temaIndicator.Run(inputs, options, outputs);
var tResult = outputs[0];
// Compare last 100 records
ValidationHelper.VerifyData(qResult, tResult, lookback, tolerance: ValidationHelper.TulipTolerance);
}
_output.WriteLine("TEMA Batch(TSeries) validated successfully against Tulip");
}
[Fact]
public void Validate_Talib_Span()
{
int[] periods = { 5, 10, 20, 50, 100 };
// Prepare data
double[] talibOutput = new double[_testData.RawData.Length];
foreach (var period in periods)
{
// Calculate QuanTAlib TEMA (Span API)
double[] qOutput = new double[_testData.RawData.Length];
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);
Assert.Equal(Core.RetCode.Success, retCode);
int lookback = TALib.Functions.TemaLookback(period);
// Compare last 100 records
ValidationHelper.VerifyData(qOutput, talibOutput, outRange, lookback, tolerance: ValidationHelper.TalibTolerance);
}
_output.WriteLine("TEMA Span validated successfully against TA-Lib");
}
}
+450
View File
@@ -0,0 +1,450 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// TEMA: Triple Exponential Moving Average
/// </summary>
/// <remarks>
/// TEMA uses triple smoothing to reduce lag even further than DEMA.
///
/// Calculation:
/// EMA1 = EMA(input)
/// EMA2 = EMA(EMA1)
/// EMA3 = EMA(EMA2)
/// TEMA = 3 * EMA1 - 3 * EMA2 + EMA3
///
/// O(1) update:
/// Uses three EMA instances, each with O(1) update complexity.
///
/// IsHot:
/// Becomes true when the TEMA step response converges to within 5% error.
/// This happens when the third EMA's error factor drops below ~9% (approx 2.43/alpha steps),
/// which is faster than the standard EMA convergence (3/alpha steps).
/// </remarks>
[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 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;
}
}
+168
View File
@@ -0,0 +1,168 @@
# TEMA: Triple Exponential Moving Average
> "Patrick Mulloy looked at the lag of an EMA and took it personally. TEMA is what happens when you apply algebra to impatience."
The Triple Exponential Moving Average (TEMA) is a lag-reducing filter that combines a single, double, and triple EMA. Unlike a simple triple smoothing (which would be incredibly slow), TEMA uses a weighted combination of the three to cancel out the lag, resulting in an indicator that hugs price action tighter than a spandex cycling short.
## Historical Context
Introduced by Patrick Mulloy in *Technical Analysis of Stocks & Commodities* (Jan 1994), "Smoothing Data With Less Lag." Mulloy's goal was to replace the standard moving averages in MACD and other indicators to reduce the delay in signal generation.
## Architecture & Physics
TEMA is not just "EMA applied three times." That would be $EMA(EMA(EMA(x)))$. TEMA is a composite:
$$ TEMA = 3 \cdot EMA_1 - 3 \cdot EMA_2 + EMA_3 $$
This formula effectively projects the trend forward to compensate for the delay inherent in smoothing.
### Convergence Speed
Because of the aggressive weighting, TEMA converges (warms up) faster than a standard EMA. While an EMA takes $\approx 3.45(N+1)$ steps to converge to 99.9%, TEMA stabilizes quicker due to the subtraction terms canceling out the initial error.
## Mathematical Foundation
### 1. The Cascade
$$ EMA_1 = EMA(Price) $$
$$ EMA_2 = EMA(EMA_1) $$
$$ EMA_3 = EMA(EMA_2) $$
### 2. The Combination
$$ TEMA = (3 \times EMA_1) - (3 \times EMA_2) + EMA_3 $$
## Performance Profile
### Operation Count (Streaming Mode)
TEMA requires 3 cascaded EMA updates plus the combination formula:
| Operation | Count | Cost (cycles) | Subtotal |
| :--- | :---: | :---: | :---: |
| EMA update (×3) | 3 | 7 | 21 |
| MUL (3×e1, 3×e2) | 2 | 3 | 6 |
| SUB (3×e1 - 3×e2) | 1 | 1 | 1 |
| ADD (+ e3) | 1 | 1 | 1 |
| **Total (hot)** | **7** | — | **~29 cycles** |
During warmup, each EMA stage has additional compensator overhead (~21 cycles × 3 = ~63 cycles).
**Total during warmup:** ~92 cycles/bar; **Post-warmup:** ~29 cycles/bar.
### Batch Mode (SIMD Analysis)
TEMA is inherently recursive due to cascaded EMAs. SIMD parallelization across bars is not possible. Each EMA stage must complete before feeding the next:
| Optimization | Operations | Cycles Saved |
| :--- | :---: | :---: |
| FMA in each EMA stage | 3 FMA vs 3×(MUL+ADD) | ~6 cycles |
| Inline combination | Avoid intermediate stores | ~2 cycles |
**Per-bar efficiency:** ~29 cycles is 4× EMA cost, as expected for 3 EMA stages + combiner.
### Quality Metrics
| Metric | Score | Notes |
| :--- | :---: | :--- |
| **Accuracy** | 10/10 | Matches TA-Lib exactly |
| **Timeliness** | 10/10 | Extremely low lag; nearly zero-lag tracking |
| **Overshoot** | 5/10 | Significant overshoot on sharp reversals |
| **Smoothness** | 6/10 | Less smooth than SMA/EMA due to high responsiveness |
### Benchmark Results
| Metric | Value | Notes |
| :--- | :--- | :--- |
| **Throughput** | ~6 ns/bar | 3× EMA overhead |
| **Allocations** | 0 bytes | Zero-allocation in hot paths |
| **Complexity** | O(1) | Constant time regardless of period |
| **State Size** | 96 bytes | Three EMA states (32 bytes each) |
## Validation
| Library | Status | Notes |
| :--- | :--- | :--- |
| **QuanTAlib** | ✅ | Validated. |
| **TA-Lib** | ✅ | Matches `TA_TEMA` exactly. |
| **Skender** | ✅ | Matches `GetTema` exactly. |
| **Tulip** | ✅ | Matches `tema` exactly. |
| **Ooples** | ❌ | Diverges significantly due to initialization logic. |
## C# Implementation Considerations
### State Management
TEMA maintains six EmaState instances—three current, three previous—enabling atomic rollback on bar corrections:
```csharp
private record struct EmaState(double Ema, double E, bool IsHot, bool IsCompensated);
private EmaState _state1, _state2, _state3;
private EmaState _p_state1, _p_state2, _p_state3;
```
The `E` field tracks bias compensation factor for each EMA stage independently. Each state auto-transitions via `IsCompensated` flag when bias becomes negligible.
### Precomputed Constants
Constructor calculates smoothing constants once:
```csharp
_alpha = 2.0 / (period + 1);
_decay = 1 - _alpha;
```
These constants are reused across all three EMA stages, avoiding repeated division.
### FMA Usage
Each EMA update uses FusedMultiplyAdd for the standard EMA formula:
```csharp
double newEma = Math.FusedMultiplyAdd(state.Ema, _decay, _alpha * input);
```
The final TEMA combination `3*e1 - 3*e2 + e3` could use FMA but the coefficients (3, -3, 1) make chained FMA marginal; current implementation uses direct arithmetic.
### Bar Correction Pattern
TEMA's cascaded structure requires coordinated state rollback:
```csharp
if (isNew)
{
_p_state1 = _state1;
_p_state2 = _state2;
_p_state3 = _state3;
}
else
{
_state1 = _p_state1;
_state2 = _p_state2;
_state3 = _p_state3;
}
```
All three stages rollback atomically, ensuring consistent cascade state when `isNew=false`.
### Memory Layout
| Field | Type | Size | Purpose |
| :--- | :--- | :---: | :--- |
| `_alpha` | double | 8B | Smoothing constant |
| `_decay` | double | 8B | 1 - alpha |
| `_state1` | EmaState | 24B | First EMA state |
| `_state2` | EmaState | 24B | Second EMA state |
| `_state3` | EmaState | 24B | Third EMA state |
| `_p_state1` | EmaState | 24B | Previous state 1 |
| `_p_state2` | EmaState | 24B | Previous state 2 |
| `_p_state3` | EmaState | 24B | Previous state 3 |
| **Total** | | **160B** | Per indicator instance |
Each EmaState contains: Ema (8B), E (8B), IsHot (1B), IsCompensated (1B) + padding (~6B) = ~24B.
### Common Pitfalls
1. **Overshoot**: TEMA is so responsive it can overshoot price turns, creating a "whiplash" effect in volatile markets.
2. **Noise**: By reducing lag, TEMA sacrifices some noise suppression. It is "nervous" compared to an SMA.
3. **Identity Crisis**: Often confused with T3 (Tillson). T3 is a generalized version; TEMA is specifically T3 with $v=1$.
+66
View File
@@ -0,0 +1,66 @@
// The MIT License (MIT)
// © mihakralj
//@version=6
indicator("Triple Exponential Moving Average (TEMA)", "TEMA", overlay=true)
//@function Calculates TEMA using triple exponential smoothing with compensator
//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/trends_IIR/tema.md
//@param source Series to calculate TEMA from
//@param period Lookback period for TEMA calculation
//@param alpha Optional smoothing factor (overrides period if provided)
//@param corrected Use diminishing alpha factors for each stage
//@returns TEMA value from first bar with proper compensation
//@optimized Uses exponential warmup compensator on all three EMA stages for O(1) complexity
tema(series float source, simple int period=0, simple float alpha=0.0, simple bool corrected=false) =>
if alpha <= 0 and period <= 0
runtime.error("Alpha or period must be provided")
float a1 = alpha > 0 ? alpha : (period > 0 ? 2.0 / (period + 1) : 0.1)
float r = math.pow(1.0 / a1, 1.0 / 3.0)
float a2 = corrected ? a1 * r : a1
float a3 = corrected ? a2 * r : a1
float beta1 = 1.0 - a1
float beta2 = 1.0 - a2
float beta3 = 1.0 - a3
var float e1 = 1.0
var float e2 = 1.0
var float e3 = 1.0
var bool warmup = true
var float rema1 = 0.0
var float rema2 = 0.0
var float rema3 = 0.0
var float ema1 = source
var float ema2 = source
var float ema3 = source
rema1 := a1 * (source - rema1) + rema1
if warmup
e1 *= beta1
e2 *= beta2
e3 *= beta3
float c1 = 1.0 / (1.0 - e1)
float c2 = 1.0 / (1.0 - e2)
float c3 = 1.0 / (1.0 - e3)
ema1 := rema1 * c1
rema2 := a2 * (ema1 - rema2) + rema2
ema2 := rema2 * c2
rema3 := a3 * (ema2 - rema3) + rema3
ema3 := rema3 * c3
warmup := e1 > 1e-10
else
ema1 := rema1
rema2 := a2 * (ema1 - rema2) + rema2
ema2 := rema2
rema3 := a3 * (ema2 - rema3) + rema3
ema3 := rema3
3 * ema1 - 3 * ema2 + ema3
// ---------- Main loop ----------
// Inputs
i_period = input.int(10, "Period", minval=1)
i_source = input.source(close, "Source")
// Calculation
tema_value = tema(i_source, period=i_period)
// Plot
plot(tema_value, "TEMA", color=color.yellow, linewidth=2)