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
[Pine Script Implementation of HMA](https://github.com/mihakralj/pinescript/blob/main/indicators/trends_FIR/hma.pine)
## 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
* **Lag Reduction:** Uses weighted moving averages (WMA) in a specific combination to offset lag.
* **Smoothness:** The final smoothing step ensures the indicator remains readable and not overly jittery.
* **Formula:** $HMA = WMA(\sqrt{n}, 2 \cdot WMA(n/2, price) - WMA(n, price))$
* **Reduced lag:** HMA substantially decreases the delay in trend identification compared to traditional moving averages
* **Smoothing preservation:** Maintains effective noise filtering despite its increased responsiveness
* **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.
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.
## Common Settings and Parameters
## 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
using QuanTAlib;
**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.
// Initialize
var hma = new Hma(14);
## Calculation and Mathematical Foundation
// Update
var result = hma.Update(new TValue(time, price));
**Simplified explanation:**
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
var series = Hma.Calculate(sourceSeries, 14);
```
**Technical formula:**
## 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).
* **Batch:** Uses SIMD-optimized WMA calculations and vector operations for the intermediate step.
* **Zero Allocation:** Span-based API available for high-performance scenarios.
Where:
* 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
* [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));
```