mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-25 22:08:05 +00:00
T3 Moving Average and associated tests
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
```
|
||||
Reference in New Issue
Block a user