T3 Moving Average and associated tests

This commit is contained in:
Miha Kralj
2025-12-07 17:10:41 -08:00
parent 875998b288
commit 94d06b0749
8 changed files with 968 additions and 25 deletions
+45 -25
View File
@@ -1,44 +1,64 @@
# HMA: Hull Moving Average # HMA: Hull Moving Average
[Pine Script Implementation of HMA](https://github.com/mihakralj/pinescript/blob/main/indicators/trends_FIR/hma.pine)
## Overview and Purpose ## Overview and Purpose
The Hull Moving Average (HMA), developed by Alan Hull in 2005, is designed to solve the age-old problem of making a moving average more responsive to current price activity while maintaining curve smoothness. It achieves this by eliminating lag almost entirely and managing to improve smoothing at the same time. The Hull Moving Average (HMA) is a technical indicator designed to significantly reduce lag while maintaining smoothness in price data interpretation. Developed by Australian mathematician and trader Alan Hull in 2005, the HMA was created specifically to address the lagging nature of conventional moving averages. Hull sought to create an indicator that maintained effective smoothing capabilities while improving responsiveness, publishing his approach in "Better Trading with the Hull Moving Average" (2005). Through its multi-stage calculation process involving weighted moving averages and square-root period weighting, HMA provides traders with a more responsive tool for identifying trends and potential reversals.
## Core Concepts ## Core Concepts
* **Lag Reduction:** Uses weighted moving averages (WMA) in a specific combination to offset lag. * **Reduced lag:** HMA substantially decreases the delay in trend identification compared to traditional moving averages
* **Smoothness:** The final smoothing step ensures the indicator remains readable and not overly jittery. * **Smoothing preservation:** Maintains effective noise filtering despite its increased responsiveness
* **Formula:** $HMA = WMA(\sqrt{n}, 2 \cdot WMA(n/2, price) - WMA(n, price))$ * **Market application:** Particularly effective for timing entries and exits in trending markets where minimizing lag is critical
* **Timeframe flexibility:** Functions effectively across all timeframes with period adjustments to suit trading style
## Calculation The core innovation of HMA is its unique three-stage process that includes weighted averaging at different timeframes, followed by a momentum-enhanced smoothing phase. By applying weight calculations at half the specified period, then taking the difference between this result and the full-period calculation, and finally smoothing that difference with a square-root weighted calculation, HMA creates a moving average that anticipates price movements rather than simply following them.
1. Calculate a WMA with period $n/2$ and multiply by 2. ## Common Settings and Parameters
2. Calculate a WMA with period $n$ and subtract from step 1.
3. Calculate a WMA with period $\sqrt{n}$ using the result of step 2.
## C# Implementation | Parameter | Default | Function | When to Adjust |
|-----------|---------|----------|---------------|
| Length | 9 | Controls the primary calculation period | Increase for smoother signals in volatile markets, decrease for more responsiveness |
| Source | close | Price data used for calculation | Consider using hlc3 for a more balanced price representation |
```csharp **Pro Tip:** Using square numbers (4, 9, 16, 25, 36) as periods can produce optimal results with HMA due to the square root operation in the final calculation step.
using QuanTAlib;
// Initialize ## Calculation and Mathematical Foundation
var hma = new Hma(14);
// Update **Simplified explanation:**
var result = hma.Update(new TValue(time, price)); HMA first calculates two weighted moving averages - one using half the specified period (which responds quickly) and one using the full period (which is smoother). It then doubles the faster WMA and subtracts the slower WMA to create a difference that emphasizes recent price direction. Finally, it applies another weighted moving average using the square root of the original period to smooth this difference.
// Batch **Technical formula:**
var series = Hma.Calculate(sourceSeries, 14);
```
## Performance 1. Calculate WMA with period n/2: WMA₁ = WMA(price, n/2)
2. Calculate WMA with period n: WMA₂ = WMA(price, n)
3. Calculate the difference: diff = 2 × WMA₁ - WMA₂
4. Calculate final HMA: HMA = WMA(diff, √n)
* **Streaming:** O(1) complexity per update (uses 3 internal O(1) WMAs). Where:
* **Batch:** Uses SIMD-optimized WMA calculations and vector operations for the intermediate step.
* **Zero Allocation:** Span-based API available for high-performance scenarios. * n is the specified period
* √n is the square root of n (rounded down)
> 🔍 **Technical Note:** The 2× multiplier applied to the faster WMA serves to amplify the momentum component, helping the HMA anticipate rather than just follow price movements.
## Interpretation Details
HMA can be used in various trading strategies:
* **Trend identification:** The direction of HMA indicates the prevailing trend
* **Signal generation:** Crossovers between price and HMA generate trade signals earlier than with traditional moving averages
* **Support/resistance levels:** HMA can act as dynamic support during uptrends and resistance during downtrends
* **Trend strength assessment:** The angle of the HMA line can indicate trend strength
* **Multiple timeframe analysis:** Using HMAs with different periods can confirm trends across different timeframes
## Limitations and Considerations
* **Market conditions:** Less effective in ranging or choppy markets where increased responsiveness may generate false signals
* **Overshooting:** The aggressive lag reduction can cause overshooting during sharp reversals
* **Amplitude distortion:** The 2× multiplier in the formula can exaggerate price movements
* **Gap sensitivity:** More prone to creating gaps in the moving average line during price gaps
* **Complementary tools:** Best used alongside momentum oscillators or volume indicators for confirmation
## References ## References
* [Alan Hull's HMA Description](https://alan.hull.com.au/hma.html) * Hull, Alan. "Better Trading with the Hull Moving Average." MTA Symposium Proceedings, 2005
+37
View File
@@ -0,0 +1,37 @@
using System;
using System.Reflection;
using Tulip;
using Xunit;
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
public class DebugTulipTests
{
private readonly ITestOutputHelper _output;
public DebugTulipTests(ITestOutputHelper output)
{
_output = output;
}
[Fact]
public void ListTulipIndicators()
{
var type = typeof(Tulip.Indicators);
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Static);
var fields = type.GetFields(BindingFlags.Public | BindingFlags.Static);
_output.WriteLine("Tulip Indicators (Properties):");
foreach (var p in properties)
{
_output.WriteLine(p.Name);
}
_output.WriteLine("Tulip Indicators (Fields):");
foreach (var f in fields)
{
_output.WriteLine(f.Name);
}
}
}
+67
View File
@@ -0,0 +1,67 @@
using System.Drawing;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
public class T3Indicator : Indicator, IWatchlistIndicator
{
[InputParameter("Period", sortIndex: 1, 1, 1000, 1, 0)]
public int Period { get; set; } = 10;
[InputParameter("Volume Factor", sortIndex: 2, 0, 1, 0.01, 2)]
public double VolumeFactor { get; set; } = 0.7;
[IndicatorExtensions.DataSourceInput]
public SourceType Source { get; set; } = SourceType.Close;
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private T3? ma;
protected LineSeries? Series;
protected string? SourceName;
private int _warmupBarIndex = -1;
public int MinHistoryDepths => Period * 6; // Approx warmup for 6 stages
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => $"T3({Period}, {VolumeFactor:F2}):{SourceName}";
public T3Indicator()
{
OnBackGround = true;
SeparateWindow = false;
SourceName = Source.ToString();
Name = "T3 - Tillson T3 Moving Average";
Description = "Tillson T3 Moving Average";
Series = new(name: $"T3 {Period}", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
AddLineSeries(Series);
}
protected override void OnInit()
{
ma = new T3(Period, VolumeFactor);
SourceName = Source.ToString();
_warmupBarIndex = -1;
base.OnInit();
}
protected override void OnUpdate(UpdateArgs args)
{
TValue input = this.GetInputValue(args, Source);
bool isNew = args.Reason == UpdateReason.NewBar || args.Reason == UpdateReason.HistoricalBar;
TValue result = ma!.Update(input, isNew);
Series!.SetValue(result.Value);
Series!.SetMarker(0, Color.Transparent);
if (_warmupBarIndex < 0 && ma!.IsHot)
_warmupBarIndex = Count;
}
public override void OnPaintChart(PaintChartEventArgs args)
{
base.OnPaintChart(args);
int warmupPeriod = _warmupBarIndex > 0 ? _warmupBarIndex : Count;
this.PaintSmoothCurve(args, Series!, warmupPeriod, showColdValues: ShowColdValues, tension: 0.2);
}
}
+104
View File
@@ -0,0 +1,104 @@
namespace QuanTAlib.Tests;
public class T3Tests
{
[Fact]
public void T3_Constructor_Period_ValidatesInput()
{
Assert.Throws<ArgumentException>(() => new T3(0));
Assert.Throws<ArgumentException>(() => new T3(-1));
var t3 = new T3(10);
Assert.NotNull(t3);
}
[Fact]
public void T3_ConstantInput_ConvergesToInput()
{
var t3 = new T3(5, 0.7);
double input = 100.0;
// Feed enough values for T3 to converge (it has 6 cascaded EMAs)
for(int i = 0; i < 100; i++)
{
t3.Update(new TValue(DateTime.UtcNow, input));
}
Assert.Equal(input, t3.Last.Value, 1e-9);
}
[Fact]
public void T3_Parameters_AffectResult()
{
// Different volume factors should produce different results for changing data
var t3_low_v = new T3(10, 0.1);
var t3_high_v = new T3(10, 0.9);
var series = new TSeries();
series.Add(DateTime.UtcNow, 100);
series.Add(DateTime.UtcNow.AddMinutes(1), 110);
series.Add(DateTime.UtcNow.AddMinutes(2), 120);
t3_low_v.Update(series);
t3_high_v.Update(series);
Assert.NotEqual(t3_low_v.Last.Value, t3_high_v.Last.Value);
}
[Fact]
public void T3_Reset_ResetsState()
{
var t3 = new T3(10);
t3.Update(new TValue(DateTime.UtcNow, 100));
t3.Update(new TValue(DateTime.UtcNow, 110));
Assert.True(t3.IsHot);
Assert.NotEqual(0, t3.Last.Value);
t3.Reset();
Assert.False(t3.IsHot);
Assert.Equal(0, t3.Last.Value);
// Should accept new data as if fresh
t3.Update(new TValue(DateTime.UtcNow, 50));
Assert.Equal(50, t3.Last.Value, 1e-9); // First value logic: output = input
}
[Fact]
public void T3_Eventing_Works()
{
var source = new TSeries();
var t3 = new T3(source, 10);
double lastVal = 0;
t3.Pub += (v) => lastVal = v.Value;
source.Add(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100, lastVal, 1e-9);
source.Add(new TValue(DateTime.UtcNow, 110));
Assert.NotEqual(100, lastVal);
Assert.NotEqual(0, lastVal);
}
[Fact]
public void T3_SpanTests()
{
var series = new TSeries();
int count = 100;
for(int i=0; i<count; i++)
series.Add(DateTime.UtcNow.AddMinutes(i), 100 + i);
var t3 = new T3(10);
var resSeries = t3.Update(series);
var resSpan = new double[count];
// Correctly use Span.CopyTo
T3.Calculate(series, 10).Values.CopyTo(resSpan.AsSpan());
// Check last values match
Assert.Equal(resSeries.Last.Value, resSpan[count-1], 1e-9);
}
}
+234
View File
@@ -0,0 +1,234 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Skender.Stock.Indicators;
using TALib;
using Tulip;
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
public class T3ValidationTests
{
private readonly TBarSeries _bars;
private readonly TSeries _data;
private readonly List<Quote> _skenderQuotes;
private readonly ITestOutputHelper _output;
public T3ValidationTests(ITestOutputHelper output)
{
_output = output;
// 1. Generate 2000 records using GBM feed
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2);
_bars = gbm.Fetch(2000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// 2. Extract Close TSeries
_data = _bars.Close;
// 3. Prepare data for Skender (List<Quote>)
_skenderQuotes = new List<Quote>();
for (int i = 0; i < _bars.Count; i++)
{
_skenderQuotes.Add(new Quote
{
Date = new DateTime(_bars.Open.Times[i], DateTimeKind.Utc),
Open = (decimal)_bars.Open[i].Value,
High = (decimal)_bars.High[i].Value,
Low = (decimal)_bars.Low[i].Value,
Close = (decimal)_bars.Close[i].Value,
Volume = (decimal)_bars.Volume[i].Value
});
}
}
[Fact]
public void Validate_Skender_Batch()
{
int[] periods = { 5, 10, 20 };
double vFactor = 0.7;
foreach (var period in periods)
{
// Calculate QuanTAlib T3
var t3 = new global::QuanTAlib.T3(period, vFactor);
var qResult = t3.Update(_data);
// Calculate Skender T3
var sResult = _skenderQuotes.GetT3(period, vFactor).ToList();
// Compare last 100 records
VerifyData_Skender(qResult, sResult);
}
_output.WriteLine("T3 Batch(TSeries) validated successfully against Skender");
}
[Fact]
public void Validate_Talib_Batch()
{
int[] periods = { 5, 10, 20 };
double vFactor = 0.7;
// Prepare data for TA-Lib
double[] tData = _data.Select(x => x.Value).ToArray();
double[] output = new double[tData.Length];
foreach (var period in periods)
{
// Calculate QuanTAlib T3
var t3 = new global::QuanTAlib.T3(period, vFactor);
var qResult = t3.Update(_data);
// Calculate TA-Lib T3
var retCode = TALib.Functions.T3<double>(tData, 0..^0, output, out var outRange, period, vFactor);
Assert.Equal(Core.RetCode.Success, retCode);
int lookback = TALib.Functions.T3Lookback(period);
// Compare last 100 records
VerifyData_Talib(qResult, output, outRange, lookback);
}
_output.WriteLine("T3 Batch(TSeries) validated successfully against TA-Lib");
}
[Fact]
public void Validate_Talib_Streaming()
{
int[] periods = { 5, 10, 20 };
double vFactor = 0.7;
// Prepare data for TA-Lib
double[] tData = _data.Select(x => x.Value).ToArray();
double[] output = new double[tData.Length];
foreach (var period in periods)
{
// Calculate QuanTAlib T3 (streaming)
var t3 = new global::QuanTAlib.T3(period, vFactor);
var qResults = new List<double>();
foreach (var item in _data)
{
qResults.Add(t3.Update(item).Value);
}
// Calculate TA-Lib T3
var retCode = TALib.Functions.T3<double>(tData, 0..^0, output, out var outRange, period, vFactor);
Assert.Equal(Core.RetCode.Success, retCode);
int lookback = TALib.Functions.T3Lookback(period);
// Compare last 100 records
VerifyData_Talib_Streaming(qResults, output, outRange, lookback);
}
_output.WriteLine("T3 Streaming validated successfully against TA-Lib");
}
[Fact]
public void Validate_Talib_Span()
{
int[] periods = { 5, 10, 20 };
double vFactor = 0.7;
// Prepare data
double[] sourceData = _data.Select(x => x.Value).ToArray();
double[] talibOutput = new double[sourceData.Length];
foreach (var period in periods)
{
// Calculate QuanTAlib T3 (Span API)
double[] qOutput = new double[sourceData.Length];
global::QuanTAlib.T3.Calculate(sourceData.AsSpan(), qOutput.AsSpan(), period, vFactor);
// Calculate TA-Lib T3
var retCode = TALib.Functions.T3<double>(sourceData, 0..^0, talibOutput, out var outRange, period, vFactor);
Assert.Equal(Core.RetCode.Success, retCode);
int lookback = TALib.Functions.T3Lookback(period);
// Compare last 100 records
VerifyData_Talib_Span(qOutput, talibOutput, outRange, lookback);
}
_output.WriteLine("T3 Span validated successfully against TA-Lib");
}
private static void VerifyData_Skender(TSeries qSeries, List<T3Result> sSeries)
{
Assert.Equal(qSeries.Count, sSeries.Count);
int count = qSeries.Count;
int skip = count - 100;
for (int i = skip; i < count; i++)
{
double qValue = qSeries[i].Value;
double? sValue = sSeries[i].T3;
if (!sValue.HasValue) continue;
Assert.Equal(sValue.Value, qValue, 1e-4);
}
}
private static void VerifyData_Talib(TSeries qSeries, double[] tOutput, Range outRange, int lookback)
{
int count = qSeries.Count;
int skip = count - 100;
int validCount = outRange.End.Value - outRange.Start.Value;
for (int i = skip; i < count; i++)
{
double qValue = qSeries[i].Value;
if (i < lookback) continue;
int tIndex = i - lookback;
if (tIndex >= validCount) continue;
double tValue = tOutput[tIndex];
Assert.Equal(tValue, qValue, 1e-4);
}
}
private static void VerifyData_Talib_Streaming(List<double> qResults, double[] tOutput, Range outRange, int lookback)
{
int count = qResults.Count;
int skip = count - 100;
int validCount = outRange.End.Value - outRange.Start.Value;
for (int i = skip; i < count; i++)
{
double qValue = qResults[i];
if (i < lookback) continue;
int tIndex = i - lookback;
if (tIndex >= validCount) continue;
double tValue = tOutput[tIndex];
Assert.Equal(tValue, qValue, 1e-4);
}
}
private static void VerifyData_Talib_Span(double[] qOutput, double[] tOutput, Range outRange, int lookback)
{
int count = qOutput.Length;
int skip = count - 100;
int validCount = outRange.End.Value - outRange.Start.Value;
for (int i = skip; i < count; i++)
{
double qValue = qOutput[i];
if (i < lookback) continue;
int tIndex = i - lookback;
if (tIndex >= validCount) continue;
double tValue = tOutput[tIndex];
Assert.Equal(tValue, qValue, 1e-4);
}
}
}
+242
View File
@@ -0,0 +1,242 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// T3: Tillson T3 Moving Average
/// </summary>
/// <remarks>
/// T3 works by running price data through a series of six EMAs, then combining the outputs
/// of these EMAs using carefully calculated weights.
///
/// Formula:
/// T3 = c1*e6 + c2*e5 + c3*e4 + c4*e3
///
/// Where:
/// e1..e6 are cascaded EMAs
/// c1 = -v^3
/// c2 = 3(v^2 + v^3)
/// c3 = -3(2v^2 + v + v^3)
/// c4 = 1 + 3v + 3v^2 + v^3
///
/// v is volume factor (default 0.7)
/// alpha = 2 / (period + 1)
/// </remarks>
[SkipLocalsInit]
public sealed class T3 : ITValuePublisher
{
private struct State
{
public double E1, E2, E3, E4, E5, E6;
public bool IsInitialized;
public static State New() => new() { IsInitialized = false };
}
private readonly double _alpha;
private readonly double _c1, _c2, _c3, _c4;
private State _state = State.New();
private State _p_state = State.New();
private double _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>
/// <param name="period">Period for EMA calculation (must be > 0)</param>
/// <param name="vfactor">Volume Factor (default 0.7)</param>
public T3(int period, double vfactor = 0.7)
{
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
_alpha = 2.0 / (period + 1);
// Precompute coefficients
double v = vfactor;
double v2 = v * v;
double v3 = v2 * v;
_c1 = -v3;
_c2 = 3.0 * (v2 + v3);
_c3 = -3.0 * (2.0 * v2 + v + v3);
_c4 = 1.0 + 3.0 * v + 3.0 * v2 + v3;
Name = $"T3({period}, {vfactor:F2})";
}
/// <summary>
/// Creates T3 with specified source, period and volume factor.
/// Subscribes to source.Pub event.
/// </summary>
/// <param name="source">Source to subscribe to</param>
/// <param name="period">Period for EMA calculation</param>
/// <param name="vfactor">Volume Factor (default 0.7)</param>
public T3(ITValuePublisher source, int period, double vfactor = 0.7) : this(period, vfactor)
{
source.Pub += (item) => Update(item);
}
/// <summary>
/// Current T3 value.
/// </summary>
public TValue Last { get; private set; }
/// <summary>
/// True if the T3 has been initialized (received at least one value).
/// </summary>
public bool IsHot => _state.IsInitialized;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double GetValidValue(double input)
{
if (double.IsFinite(input))
{
_lastValidValue = input;
return input;
}
return _lastValidValue;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
_p_state = _state;
}
else
{
_state = _p_state;
}
double val = GetValidValue(input.Value);
val = Compute(val, _alpha, _c1, _c2, _c3, _c4, ref _state);
Last = new TValue(input.Time, val);
Pub?.Invoke(Last);
return Last;
}
public TSeries Update(TSeries source)
{
if (source.Count == 0) return new TSeries(new List<long>(), new List<double>());
int len = source.Count;
var t = new List<long>(len);
var v = new List<double>(len);
CollectionsMarshal.SetCount(t, len);
CollectionsMarshal.SetCount(v, len);
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
var sourceValues = source.Values;
var sourceTimes = source.Times;
State state = _state;
double lastValidValue = _lastValidValue;
CalculateCore(sourceValues, vSpan, _alpha, _c1, _c2, _c3, _c4, ref state, ref lastValidValue);
_state = state;
_lastValidValue = lastValidValue;
sourceTimes.CopyTo(tSpan);
_p_state = _state;
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 c1, double c2, double c3, double c4, ref State state)
{
if (!state.IsInitialized)
{
state.E1 = state.E2 = state.E3 = state.E4 = state.E5 = state.E6 = input;
state.IsInitialized = true;
}
else
{
state.E1 += alpha * (input - state.E1);
state.E2 += alpha * (state.E1 - state.E2);
state.E3 += alpha * (state.E2 - state.E3);
state.E4 += alpha * (state.E3 - state.E4);
state.E5 += alpha * (state.E4 - state.E5);
state.E6 += alpha * (state.E5 - state.E6);
}
return c1 * state.E6 + c2 * state.E5 + c3 * state.E4 + c4 * state.E3;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void CalculateCore(ReadOnlySpan<double> source, Span<double> output, double alpha,
double c1, double c2, double c3, double c4, ref State state, ref double lastValidValue)
{
int len = source.Length;
for (int i = 0; i < len; i++)
{
double val = source[i];
if (double.IsFinite(val))
lastValidValue = val;
else
val = lastValidValue;
output[i] = Compute(val, alpha, c1, c2, c3, c4, ref state);
}
}
/// <summary>
/// Calculates T3 for the entire series using a new instance.
/// </summary>
public static TSeries Calculate(TSeries source, int period, double vfactor = 0.7)
{
var t3 = new T3(period, vfactor);
return t3.Update(source);
}
/// <summary>
/// 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)
{
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
if (source.Length != output.Length)
throw new ArgumentException("Source and output must have the same length");
double alpha = 2.0 / (period + 1);
double v = vfactor;
double v2 = v * v;
double v3 = v2 * v;
double c1 = -v3;
double c2 = 3.0 * (v2 + v3);
double c3 = -3.0 * (2.0 * v2 + v + v3);
double c4 = 1.0 + 3.0 * v + 3.0 * v2 + v3;
State state = State.New();
double lastValidValue = 0;
CalculateCore(source, output, alpha, c1, c2, c3, c4, ref state, ref lastValidValue);
}
/// <summary>
/// Resets the T3 state.
/// </summary>
public void Reset()
{
_state = State.New();
_p_state = _state;
_lastValidValue = 0;
Last = default;
}
}
+66
View File
@@ -0,0 +1,66 @@
# T3: Tillson T3 Moving Average
## Overview and Purpose
The Tillson T3 Moving Average is an advanced technical indicator designed to provide superior smoothing with minimal lag. Developed by Tim Tillson and introduced in the January 1998 issue of Technical Analysis of Stocks & Commodities magazine, T3 implements a sophisticated six-stage EMA architecture with optimized coefficient distribution based on a volume factor parameter.
Unlike simpler moving averages or even triple-EMA approaches, T3 uses a unique mathematical framework that strategically combines multiple EMAs with precisely calculated coefficients. This approach creates a moving average that effectively reduces noise while preserving important trend information and minimizing lag.
## Core Concepts
* **Multi-stage smoothing:** Uses a six-stage EMA cascade with optimized coefficient distribution to achieve superior noise reduction while minimizing lag
* **Volume factor customization:** Provides a parameter that allows traders to fine-tune the balance between smoothness and responsiveness
* **Strategic coefficient weighting:** Employs a sophisticated formula that prevents overshooting at turning points while maintaining responsiveness
## Calculation and Mathematical Foundation
T3 works by running price data through a series of six EMAs, then combining the outputs of these EMAs using carefully calculated weights. These weights are determined by a "volume factor" parameter ($v$) that controls how much the indicator prioritizes smoothness versus responsiveness.
### Formula
$$ T3 = c_1 \cdot EMA_6 + c_2 \cdot EMA_5 + c_3 \cdot EMA_4 + c_4 \cdot EMA_3 $$
Where:
* $EMA_1$ through $EMA_6$ are exponential moving averages applied in sequence:
* $EMA_1(x) = EMA(x)$
* $EMA_n(x) = EMA(EMA_{n-1}(x))$
* Coefficients are derived from the volume factor $v$:
* $c_1 = -v^3$
* $c_2 = 3(v^2 + v^3)$
* $c_3 = -3(2v^2 + v + v^3)$
* $c_4 = 1 + 3v + 3v^2 + v^3$
* Default volume factor $v = 0.7$
## Parameters
| Parameter | Default | Range | Description |
|-----------|---------|-------|-------------|
| Period | 10 | > 0 | The smoothing period for the internal EMAs |
| Volume Factor | 0.7 | 0-1 | Controls responsiveness vs smoothness (0.618 is also a common value) |
## C# Usage
### Standard TSeries Usage
```csharp
// Calculate T3 with period 10 and default volume factor 0.7
var t3 = T3.Calculate(sourceSeries, 10);
// Calculate T3 with period 10 and volume factor 0.618
var t3_custom = T3.Calculate(sourceSeries, 10, 0.618);
Console.WriteLine($"T3 Value: {t3.Last.Value}");
```
### Eventing and Reactive Support
The `T3` class implements `ITValuePublisher`, allowing for event-driven updates.
```csharp
// Create a publisher source
var source = new TValuePublisher();
// Create T3 consumer attached to the source
var t3 = new T3(source, period: 10, vfactor: 0.7);
// Handle updates
t3.Pub += (result) => {
Console.WriteLine($"New T3 Value: {result.Value} at {result.Time}");
};
// Push new values to source
source.Publish(new TValue(DateTime.UtcNow, 100.0));
```
+173
View File
@@ -0,0 +1,173 @@
using Xunit;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib.Tests;
public class T3IndicatorTests
{
[Fact]
public void T3Indicator_Constructor_SetsDefaults()
{
var indicator = new T3Indicator();
Assert.Equal(10, indicator.Period);
Assert.Equal(0.7, indicator.VolumeFactor);
Assert.Equal(SourceType.Close, indicator.Source);
Assert.True(indicator.ShowColdValues);
Assert.Equal("T3 - Tillson T3 Moving Average", indicator.Name);
Assert.False(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void T3Indicator_MinHistoryDepths_EqualsSixTimesPeriod()
{
var indicator = new T3Indicator { Period = 10 };
// MinHistoryDepths is Period * 6 for T3 due to 6 stages
Assert.Equal(60, indicator.MinHistoryDepths);
Assert.Equal(60, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
[Fact]
public void T3Indicator_ShortName_IncludesPeriodAndFactor()
{
var indicator = new T3Indicator { Period = 15, VolumeFactor = 0.618 };
Assert.Contains("T3", indicator.ShortName);
Assert.Contains("15", indicator.ShortName);
Assert.Contains("0.62", indicator.ShortName); // F2 formatting
}
[Fact]
public void T3Indicator_Initialize_CreatesInternalT3()
{
var indicator = new T3Indicator { Period = 10 };
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void T3Indicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new T3Indicator { Period = 3 };
indicator.Initialize();
// Add historical data
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
// Process update
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
// Line series should have a value
Assert.Equal(1, indicator.LinesSeries[0].Count);
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
}
[Fact]
public void T3Indicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new T3Indicator { Period = 3 };
indicator.Initialize();
// Add historical data
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
// Process first update
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
// Line series should have values
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
[Fact]
public void T3Indicator_ProcessUpdate_NewTick_ProcessesWithoutError()
{
var indicator = new T3Indicator { Period = 3 };
indicator.Initialize();
// Add historical data
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
// Process historical bar first
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
double firstValue = indicator.LinesSeries[0].GetValue(0);
// Update with new tick (same bar data - simulates intrabar update)
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
double secondValue = indicator.LinesSeries[0].GetValue(0);
// Both values should be finite
Assert.True(double.IsFinite(firstValue));
Assert.True(double.IsFinite(secondValue));
}
[Fact]
public void T3Indicator_MultipleUpdates_ProducesCorrectT3Sequence()
{
var indicator = new T3Indicator { Period = 3 };
indicator.Initialize();
var now = DateTime.UtcNow;
double[] closes = { 100, 102, 104, 103, 105, 107, 106 };
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)));
}
double lastT3 = indicator.LinesSeries[0].GetValue(0);
Assert.True(lastT3 >= 100 && lastT3 <= 110);
}
[Fact]
public void T3Indicator_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 T3Indicator { 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 T3Indicator_Parameters_CanBeChanged()
{
var indicator = new T3Indicator { Period = 5, VolumeFactor = 0.5 };
Assert.Equal(5, indicator.Period);
Assert.Equal(0.5, indicator.VolumeFactor);
indicator.Period = 20;
indicator.VolumeFactor = 0.9;
Assert.Equal(20, indicator.Period);
Assert.Equal(0.9, indicator.VolumeFactor);
Assert.Equal(120, indicator.MinHistoryDepths); // 20 * 6
}
}