mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-25 22:08:05 +00:00
Add unit tests for various moving average indicators
- Implement tests for HMA (Hull Moving Average) indicator to verify default settings, history depth calculations, and value computations during updates. - Create tests for KAMA (Kaufman Adaptive Moving Average) indicator, ensuring correct defaults, history depth, and value calculations. - Add tests for SMA (Simple Moving Average) indicator, checking default values, history depth, and value computations. - Develop tests for T3 (Tillson T3 Moving Average) indicator, validating defaults, history depth, and value calculations. - Implement tests for TEMA (Triple Exponential Moving Average) indicator, ensuring correct defaults and value computations. - Create tests for TRIMA (Triangular Moving Average) indicator, verifying defaults, history depth, and value calculations. - Add tests for WMA (Weighted Moving Average) indicator, checking default values, history depth, and value computations.
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
using System.Drawing;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class TrimaIndicator : 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 Trima? ma;
|
||||
protected LineSeries? Series;
|
||||
protected string? SourceName;
|
||||
private int _warmupBarIndex = -1;
|
||||
|
||||
public int MinHistoryDepths => Period;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"TRIMA {Period}:{SourceName}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/trends/trima/Trima.Quantower.cs";
|
||||
|
||||
public TrimaIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
SourceName = Source.ToString();
|
||||
Name = "TRIMA - Triangular Moving Average";
|
||||
Description = "Triangular Moving Average";
|
||||
Series = new(name: $"TRIMA {Period}", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(Series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
ma = new Trima(Period);
|
||||
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,242 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class TrimaTests
|
||||
{
|
||||
[Fact]
|
||||
public void Trima_Constructor_ValidatesInput()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Trima(0));
|
||||
Assert.Throws<ArgumentException>(() => new Trima(-1));
|
||||
|
||||
var trima = new Trima(10);
|
||||
Assert.NotNull(trima);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Trima_Calc_ReturnsValue()
|
||||
{
|
||||
var trima = new Trima(10);
|
||||
|
||||
Assert.Equal(0, trima.Last.Value);
|
||||
|
||||
TValue result = trima.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
Assert.True(result.Value > 0);
|
||||
Assert.Equal(result.Value, trima.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Trima_CalculatesCorrectAverage_Period4()
|
||||
{
|
||||
// Period 4 -> weights [1, 2, 2, 1], sum 6
|
||||
var trima = new Trima(4);
|
||||
|
||||
trima.Update(new TValue(DateTime.UtcNow, 10));
|
||||
trima.Update(new TValue(DateTime.UtcNow, 20));
|
||||
trima.Update(new TValue(DateTime.UtcNow, 30));
|
||||
var r1 = trima.Update(new TValue(DateTime.UtcNow, 40));
|
||||
|
||||
// (1*10 + 2*20 + 2*30 + 1*40) / 6 = 150 / 6 = 25
|
||||
Assert.Equal(25.0, r1.Value, 1e-10);
|
||||
|
||||
var r2 = trima.Update(new TValue(DateTime.UtcNow, 50));
|
||||
// (1*20 + 2*30 + 2*40 + 1*50) / 6 = 210 / 6 = 35
|
||||
Assert.Equal(35.0, r2.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Trima_CalculatesCorrectAverage_Period5()
|
||||
{
|
||||
// Period 5 -> weights [1, 2, 3, 2, 1], sum 9
|
||||
var trima = new Trima(5);
|
||||
|
||||
trima.Update(new TValue(DateTime.UtcNow, 10));
|
||||
trima.Update(new TValue(DateTime.UtcNow, 20));
|
||||
trima.Update(new TValue(DateTime.UtcNow, 30));
|
||||
trima.Update(new TValue(DateTime.UtcNow, 40));
|
||||
var r1 = trima.Update(new TValue(DateTime.UtcNow, 50));
|
||||
|
||||
// (1*10 + 2*20 + 3*30 + 2*40 + 1*50) / 9 = (10 + 40 + 90 + 80 + 50) / 9 = 270 / 9 = 30
|
||||
Assert.Equal(30.0, r1.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Trima_IsHot_BecomesTrueWhenPeriodFilled()
|
||||
{
|
||||
var trima = new Trima(4);
|
||||
|
||||
Assert.False(trima.IsHot);
|
||||
trima.Update(new TValue(DateTime.UtcNow, 10)); // 1
|
||||
Assert.False(trima.IsHot);
|
||||
trima.Update(new TValue(DateTime.UtcNow, 20)); // 2
|
||||
Assert.False(trima.IsHot);
|
||||
trima.Update(new TValue(DateTime.UtcNow, 30)); // 3
|
||||
Assert.False(trima.IsHot);
|
||||
trima.Update(new TValue(DateTime.UtcNow, 40)); // 4
|
||||
Assert.True(trima.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Trima_Update_IsNew_False_UpdatesValue()
|
||||
{
|
||||
var trima = new Trima(4);
|
||||
|
||||
trima.Update(new TValue(DateTime.UtcNow, 10));
|
||||
trima.Update(new TValue(DateTime.UtcNow, 20));
|
||||
trima.Update(new TValue(DateTime.UtcNow, 30));
|
||||
|
||||
// Update with 40
|
||||
double val1 = trima.Update(new TValue(DateTime.UtcNow, 40), isNew: true).Value;
|
||||
// Expected: 25 (as calculated above)
|
||||
Assert.Equal(25.0, val1, 1e-10);
|
||||
|
||||
// Correct last value to 100 (was 40)
|
||||
// New window: 10, 20, 30, 100
|
||||
// Weights: 1, 2, 2, 1
|
||||
// (10 + 40 + 60 + 100) / 6 = 210 / 6 = 35
|
||||
double val2 = trima.Update(new TValue(DateTime.UtcNow, 100), isNew: false).Value;
|
||||
|
||||
Assert.Equal(35.0, val2, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Trima_Reset_ClearsState()
|
||||
{
|
||||
var trima = new Trima(5);
|
||||
|
||||
trima.Update(new TValue(DateTime.UtcNow, 100));
|
||||
trima.Update(new TValue(DateTime.UtcNow, 105));
|
||||
|
||||
trima.Reset();
|
||||
|
||||
Assert.Equal(0, trima.Last.Value);
|
||||
Assert.False(trima.IsHot);
|
||||
|
||||
// After reset, should accept new values
|
||||
trima.Update(new TValue(DateTime.UtcNow, 50));
|
||||
Assert.NotEqual(0, trima.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Trima_NaN_Input_UsesLastValidValue()
|
||||
{
|
||||
var trima = new Trima(5);
|
||||
|
||||
trima.Update(new TValue(DateTime.UtcNow, 100));
|
||||
trima.Update(new TValue(DateTime.UtcNow, 110));
|
||||
|
||||
// Feed NaN - should use last valid value (110)
|
||||
var resultAfterNaN = trima.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
|
||||
Assert.True(double.IsFinite(resultAfterNaN.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Trima_BatchCalc_MatchesIterativeCalc()
|
||||
{
|
||||
var trimaIterative = new Trima(10);
|
||||
var trimaBatch = new Trima(10);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
|
||||
// Generate data
|
||||
var series = new TSeries();
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
series.Add(bar.Time, bar.Close);
|
||||
}
|
||||
|
||||
// Calculate iteratively
|
||||
var iterativeResults = new TSeries();
|
||||
#pragma warning disable S4158 // Collection is known to be empty
|
||||
foreach (var item in series)
|
||||
{
|
||||
iterativeResults.Add(trimaIterative.Update(item));
|
||||
}
|
||||
#pragma warning restore S4158
|
||||
|
||||
// Calculate batch
|
||||
var batchResults = trimaBatch.Update(series);
|
||||
|
||||
// Compare
|
||||
Assert.Equal(iterativeResults.Count, batchResults.Count);
|
||||
#pragma warning disable S2583 // Condition always evaluates to false
|
||||
for (int i = 0; i < iterativeResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(iterativeResults[i].Value, batchResults[i].Value, 1e-10);
|
||||
}
|
||||
#pragma warning restore S2583
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Trima_SpanCalc_MatchesTSeriesCalc()
|
||||
{
|
||||
var series = new TSeries();
|
||||
double[] source = new double[100];
|
||||
double[] output = new double[100];
|
||||
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
source[i] = bar.Close;
|
||||
series.Add(bar.Time, bar.Close);
|
||||
}
|
||||
|
||||
// Calculate with TSeries API
|
||||
var tseriesResult = Trima.Calculate(series, 10);
|
||||
|
||||
// Calculate with Span API
|
||||
Trima.Calculate(source.AsSpan(), output.AsSpan(), 10);
|
||||
|
||||
// Compare results
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
Assert.Equal(tseriesResult[i].Value, output[i], 1e-10);
|
||||
}
|
||||
}
|
||||
[Fact]
|
||||
public void Trima_AllModes_ProduceSameResult()
|
||||
{
|
||||
// Arrange
|
||||
int period = 10;
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
|
||||
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
// 1. Batch Mode
|
||||
var batchSeries = Trima.Calculate(series, period);
|
||||
double expected = batchSeries.Last.Value;
|
||||
|
||||
// 2. Span Mode
|
||||
var tValues = series.Values.ToArray();
|
||||
var spanInput = new ReadOnlySpan<double>(tValues);
|
||||
var spanOutput = new double[tValues.Length];
|
||||
Trima.Calculate(spanInput, spanOutput, period);
|
||||
double spanResult = spanOutput[^1];
|
||||
|
||||
// 3. Streaming Mode
|
||||
var streamingInd = new Trima(period);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
streamingInd.Update(series[i]);
|
||||
}
|
||||
double streamingResult = streamingInd.Last.Value;
|
||||
|
||||
// 4. Eventing Mode
|
||||
var pubSource = new TSeries();
|
||||
var eventingInd = new Trima(pubSource, period);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
pubSource.Add(series[i]);
|
||||
}
|
||||
double eventingResult = eventingInd.Last.Value;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, spanResult, precision: 9);
|
||||
Assert.Equal(expected, streamingResult, precision: 9);
|
||||
Assert.Equal(expected, eventingResult, precision: 9);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Skender.Stock.Indicators;
|
||||
using TALib;
|
||||
using Tulip;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class TrimaValidationTests
|
||||
{
|
||||
private readonly TBarSeries _bars;
|
||||
private readonly TSeries _data;
|
||||
private readonly List<Quote> _skenderQuotes;
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public TrimaValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
|
||||
// 1. Generate 5000 records using GBM feed
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2);
|
||||
_bars = gbm.Fetch(5000, 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, 50, 100 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate QuanTAlib TRIMA (batch TSeries)
|
||||
var trima = new global::QuanTAlib.Trima(period);
|
||||
var qResult = trima.Update(_data);
|
||||
|
||||
// Calculate Skender Composite TRIMA: SMA(SMA(x, p1), p2)
|
||||
int p1 = period / 2 + 1;
|
||||
int p2 = (period + 1) / 2;
|
||||
|
||||
var sma1Results = _skenderQuotes.GetSma(p1).ToList();
|
||||
|
||||
// Map SMA1 results to Quotes for the second pass
|
||||
// Note: We use 0 for null values during warmup, which might affect early values
|
||||
// but should stabilize for the verification window (last 100 records)
|
||||
var quotes2 = sma1Results.Select(r => new Quote
|
||||
{
|
||||
Date = r.Date,
|
||||
Close = (decimal)(r.Sma ?? 0)
|
||||
}).ToList();
|
||||
|
||||
var sResult = quotes2.GetSma(p2).ToList();
|
||||
|
||||
// Compare last 100 records
|
||||
VerifyData_Skender(qResult, sResult);
|
||||
}
|
||||
_output.WriteLine("TRIMA Batch(TSeries) validated successfully against Skender Composite SMA");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Talib_Batch()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
|
||||
// Prepare data for TA-Lib (double[])
|
||||
double[] tData = _data.Select(x => x.Value).ToArray();
|
||||
double[] output = new double[tData.Length];
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate QuanTAlib TRIMA (batch TSeries)
|
||||
var trima = new global::QuanTAlib.Trima(period);
|
||||
var qResult = trima.Update(_data);
|
||||
|
||||
// Calculate TA-Lib TRIMA
|
||||
var retCode = TALib.Functions.Trima<double>(tData, 0..^0, output, out var outRange, period);
|
||||
Assert.Equal(Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = TALib.Functions.TrimaLookback(period);
|
||||
|
||||
// Compare last 100 records
|
||||
VerifyData_Talib(qResult, output, outRange, lookback);
|
||||
}
|
||||
_output.WriteLine("TRIMA Batch(TSeries) validated successfully against TA-Lib");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Tulip_Batch()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
|
||||
// Prepare data for Tulip (double[])
|
||||
double[] tData = _data.Select(x => x.Value).ToArray();
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate QuanTAlib TRIMA (batch TSeries)
|
||||
var trima = new global::QuanTAlib.Trima(period);
|
||||
var qResult = trima.Update(_data);
|
||||
|
||||
// Calculate Tulip TRIMA
|
||||
var trimaIndicator = Tulip.Indicators.trima;
|
||||
double[][] inputs = { tData };
|
||||
double[] options = { period };
|
||||
// Tulip TRIMA lookback might be different, let's calculate or infer
|
||||
// Usually it's period-1 for simple averages, but TRIMA is double smoothed.
|
||||
// We'll rely on the output length to align.
|
||||
// Tulip.Indicators.trima.Run expects outputs to be sized correctly.
|
||||
// We can try to run it with a large buffer and see what happens,
|
||||
// or calculate the expected lookback.
|
||||
// For TRIMA(n), lookback is roughly n-1.
|
||||
int lookback = period - 1;
|
||||
double[][] outputs = { new double[tData.Length - lookback] };
|
||||
|
||||
trimaIndicator.Run(inputs, options, outputs);
|
||||
var tResult = outputs[0];
|
||||
|
||||
// Compare last 100 records
|
||||
VerifyData_Tulip(qResult, tResult, lookback);
|
||||
}
|
||||
_output.WriteLine("TRIMA Batch(TSeries) validated successfully against Tulip");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Talib_Span()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
|
||||
// Prepare data
|
||||
double[] sourceData = _data.Select(x => x.Value).ToArray();
|
||||
double[] talibOutput = new double[sourceData.Length];
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate QuanTAlib TRIMA (Span API)
|
||||
double[] qOutput = new double[sourceData.Length];
|
||||
global::QuanTAlib.Trima.Calculate(sourceData.AsSpan(), qOutput.AsSpan(), period);
|
||||
|
||||
// Calculate TA-Lib TRIMA
|
||||
var retCode = TALib.Functions.Trima<double>(sourceData, 0..^0, talibOutput, out var outRange, period);
|
||||
Assert.Equal(Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = TALib.Functions.TrimaLookback(period);
|
||||
|
||||
// Compare last 100 records
|
||||
VerifyData_Talib_Span(qOutput, talibOutput, outRange, lookback);
|
||||
}
|
||||
_output.WriteLine("TRIMA Span validated successfully against TA-Lib");
|
||||
}
|
||||
|
||||
// ==================== Verification Helpers ====================
|
||||
|
||||
private static void VerifyData_Skender(TSeries qSeries, List<SmaResult> 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].Sma;
|
||||
|
||||
if (!sValue.HasValue) continue;
|
||||
|
||||
Assert.Equal(sValue.Value, qValue, 1e-6);
|
||||
}
|
||||
}
|
||||
|
||||
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-6);
|
||||
}
|
||||
}
|
||||
|
||||
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-6);
|
||||
}
|
||||
}
|
||||
|
||||
private static void VerifyData_Tulip(TSeries qSeries, double[] tOutput, int lookback)
|
||||
{
|
||||
int count = qSeries.Count;
|
||||
int skip = count - 100;
|
||||
|
||||
for (int i = skip; i < count; i++)
|
||||
{
|
||||
double qValue = qSeries[i].Value;
|
||||
|
||||
if (i < lookback) continue;
|
||||
|
||||
int tIndex = i - lookback;
|
||||
if (tIndex >= tOutput.Length) continue;
|
||||
|
||||
double tValue = tOutput[tIndex];
|
||||
|
||||
Assert.Equal(tValue, qValue, 1e-6);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// TRIMA: Triangular Moving Average
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// TRIMA applies triangular weighting to data points, emphasizing the middle of the window.
|
||||
/// Equivalent to a double SMA: SMA(SMA(period1), period2).
|
||||
///
|
||||
/// Calculation:
|
||||
/// p1 = period / 2 + 1
|
||||
/// p2 = (period + 1) / 2
|
||||
/// TRIMA = SMA(SMA(input, p1), p2)
|
||||
///
|
||||
/// O(1) update:
|
||||
/// Uses two SMA instances, each with O(1) update complexity.
|
||||
///
|
||||
/// IsHot:
|
||||
/// Becomes true when the buffer is full (period samples processed).
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Trima : ITValuePublisher
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly int _p1;
|
||||
private readonly int _p2;
|
||||
private readonly RingBuffer _buffer1;
|
||||
private readonly RingBuffer _buffer2;
|
||||
|
||||
private double _sum1, _p_sum1, _p_lastInput1, _lastValidValue1, _p_lastValidValue1;
|
||||
private int _tickCount1;
|
||||
|
||||
private double _sum2, _p_sum2, _p_lastInput2;
|
||||
private int _tickCount2;
|
||||
|
||||
private int _sampleCount;
|
||||
private const int ResyncInterval = 1000;
|
||||
|
||||
public string Name { get; }
|
||||
public TValue Last { get; private set; }
|
||||
public bool IsHot => _sampleCount >= _period;
|
||||
public event Action<TValue>? Pub;
|
||||
|
||||
public Trima(int period)
|
||||
{
|
||||
if (period <= 0) throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
|
||||
_period = period;
|
||||
_p1 = period / 2 + 1;
|
||||
_p2 = (period + 1) / 2;
|
||||
|
||||
_buffer1 = new RingBuffer(_p1);
|
||||
_buffer2 = new RingBuffer(_p2);
|
||||
|
||||
Name = $"Trima({period})";
|
||||
}
|
||||
|
||||
public Trima(ITValuePublisher source, int period) : this(period)
|
||||
{
|
||||
source.Pub += (item) => Update(item);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double GetValidValue(double input)
|
||||
{
|
||||
if (double.IsFinite(input))
|
||||
{
|
||||
_lastValidValue1 = input;
|
||||
return input;
|
||||
}
|
||||
return _lastValidValue1;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_sampleCount++;
|
||||
|
||||
// SMA 1
|
||||
double val1 = GetValidValue(input.Value);
|
||||
double removed1 = _buffer1.Count == _buffer1.Capacity ? _buffer1.Oldest : 0.0;
|
||||
_sum1 = _sum1 - removed1 + val1;
|
||||
_buffer1.Add(val1);
|
||||
|
||||
_tickCount1++;
|
||||
if (_buffer1.IsFull && _tickCount1 >= ResyncInterval)
|
||||
{
|
||||
_tickCount1 = 0;
|
||||
_sum1 = _buffer1.Sum();
|
||||
}
|
||||
|
||||
_p_sum1 = _sum1;
|
||||
_p_lastInput1 = val1;
|
||||
_p_lastValidValue1 = _lastValidValue1;
|
||||
|
||||
double sma1Result = _sum1 / _buffer1.Count;
|
||||
|
||||
// SMA 2
|
||||
double removed2 = _buffer2.Count == _buffer2.Capacity ? _buffer2.Oldest : 0.0;
|
||||
_sum2 = _sum2 - removed2 + sma1Result;
|
||||
_buffer2.Add(sma1Result);
|
||||
|
||||
_tickCount2++;
|
||||
if (_buffer2.IsFull && _tickCount2 >= ResyncInterval)
|
||||
{
|
||||
_tickCount2 = 0;
|
||||
_sum2 = _buffer2.Sum();
|
||||
}
|
||||
|
||||
_p_sum2 = _sum2;
|
||||
_p_lastInput2 = sma1Result;
|
||||
|
||||
Last = new TValue(input.Time, _sum2 / _buffer2.Count);
|
||||
}
|
||||
else
|
||||
{
|
||||
// SMA 1 Correction
|
||||
_lastValidValue1 = _p_lastValidValue1;
|
||||
double val1 = GetValidValue(input.Value);
|
||||
_sum1 = _p_sum1 - _p_lastInput1 + val1;
|
||||
_buffer1.UpdateNewest(val1);
|
||||
|
||||
double sma1Result = _sum1 / _buffer1.Count;
|
||||
|
||||
// SMA 2 Correction
|
||||
_sum2 = _p_sum2 - _p_lastInput2 + sma1Result;
|
||||
_buffer2.UpdateNewest(sma1Result);
|
||||
|
||||
Last = new TValue(input.Time, _sum2 / _buffer2.Count);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
Calculate(source.Values, vSpan, _period);
|
||||
source.Times.CopyTo(tSpan);
|
||||
|
||||
// Restore state
|
||||
int lookback = _p1 + _p2;
|
||||
int startIndex = Math.Max(0, len - lookback);
|
||||
Reset();
|
||||
|
||||
for (int i = startIndex; i < len; i++)
|
||||
{
|
||||
Update(new TValue(source.Times[i], source.Values[i]), isNew: true);
|
||||
}
|
||||
|
||||
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
public static TSeries Calculate(TSeries source, int period)
|
||||
{
|
||||
var trima = new Trima(period);
|
||||
return trima.Update(source);
|
||||
}
|
||||
|
||||
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period)
|
||||
{
|
||||
if (source.Length != output.Length)
|
||||
throw new ArgumentException("Source and output must have the same length");
|
||||
if (period <= 0)
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
|
||||
int p1 = period / 2 + 1;
|
||||
int p2 = (period + 1) / 2;
|
||||
|
||||
double[] tempArray = ArrayPool<double>.Shared.Rent(source.Length);
|
||||
Span<double> tempSpan = tempArray.AsSpan(0, source.Length);
|
||||
|
||||
try
|
||||
{
|
||||
Sma.Calculate(source, tempSpan, p1);
|
||||
Sma.Calculate(tempSpan, output, p2);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<double>.Shared.Return(tempArray);
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_buffer1.Clear();
|
||||
_buffer2.Clear();
|
||||
|
||||
_sum1 = _p_sum1 = _p_lastInput1 = _lastValidValue1 = _p_lastValidValue1 = 0;
|
||||
_tickCount1 = 0;
|
||||
|
||||
_sum2 = _p_sum2 = _p_lastInput2 = 0;
|
||||
_tickCount2 = 0;
|
||||
|
||||
_sampleCount = 0;
|
||||
Last = default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
# TRIMA: Triangular Moving Average
|
||||
|
||||
## Overview and Purpose
|
||||
|
||||
The Triangular Moving Average (TRIMA) is a technical indicator that applies a triangular weighting scheme to price data, providing enhanced smoothing compared to simpler moving averages. Originating in the early 1970s as technical analysts sought more effective noise filtering methods, the TRIMA was first popularized through the work of market technician Arthur Merrill. Its formal mathematical properties were established in the 1980s, and the indicator gained widespread adoption in the 1990s as computerized charting became standard. TRIMA effectively filters out market noise while maintaining important trends through its unique center-weighted calculation method.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
* **Double-smoothing process:** TRIMA can be viewed as applying a simple moving average twice, creating more effective noise filtering
|
||||
* **Triangular weighting:** Uses a symmetrical weight distribution that emphasizes central data points and reduces emphasis toward both ends
|
||||
* **Market application:** Particularly effective for identifying the underlying trend in noisy market conditions where standard moving averages generate too many false signals
|
||||
* **Timeframe flexibility:** Works across multiple timeframes, with longer periods providing cleaner trend signals in higher timeframes
|
||||
|
||||
The core innovation of TRIMA is its unique triangular weighting scheme, which can be viewed either as a specialized weight distribution or as a twice-applied simple moving average with adjusted period. This creates more effective noise filtering without the excessive lag penalty typically associated with longer-period averages. The symmetrical nature of the weight distribution ensures zero phase distortion, preserving the timing of important market turning points.
|
||||
|
||||
## Common Settings and Parameters
|
||||
|
||||
| Parameter | Default | Function | When to Adjust |
|
||||
|-----------|---------|----------|---------------|
|
||||
| Length | 14 | Controls the lookback period | Increase for smoother signals in volatile markets, decrease for responsiveness |
|
||||
| Source | close | Price data used for calculation | Consider using hlc3 for a more balanced price representation |
|
||||
|
||||
**Pro Tip:** For a good balance between smoothing and responsiveness, try using a TRIMA with period N instead of an SMA with period 2N - you'll get similar smoothing characteristics but with less lag.
|
||||
|
||||
## Calculation and Mathematical Foundation
|
||||
|
||||
**Simplified explanation:**
|
||||
TRIMA calculates a weighted average of prices where the weights form a triangle shape. The middle prices get the most weight, and weights gradually decrease toward both the recent and older ends. This creates a smooth filter that effectively removes random price fluctuations while preserving the underlying trend.
|
||||
|
||||
**Technical formula:**
|
||||
TRIMA = Σ(Price[i] × Weight[i]) / Σ(Weight[i])
|
||||
|
||||
Where the triangular weights form a symmetric pattern:
|
||||
|
||||
* Weight[i] = min(i, n-1-i) + 1
|
||||
* Example for n=5: weights = [1,2,3,2,1]
|
||||
* Example for n=4: weights = [1,2,2,1]
|
||||
|
||||
Alternatively, TRIMA can be calculated as:
|
||||
TRIMA(source, p) = SMA(SMA(source, (p+1)/2), (p+1)/2)
|
||||
|
||||
> 🔍 **Technical Note:** The double application of SMA explains why TRIMA provides better smoothing than a single SMA or WMA. This approach effectively applies smoothing twice with optimal period adjustment, creating a -18dB/octave roll-off in the frequency domain compared to -6dB/octave for a simple moving average.
|
||||
|
||||
## C# Implementation
|
||||
|
||||
### Eventing and Reactive Support
|
||||
|
||||
This indicator implements the `ITValuePublisher` interface, enabling event-driven and reactive workflows.
|
||||
|
||||
* **Subscription:** Can be constructed with an `ITValuePublisher` (e.g., `TSeries`) to automatically update when the source emits a new value.
|
||||
* **Publication:** Emits a `Pub` event with the new `TValue` whenever it is updated.
|
||||
|
||||
```csharp
|
||||
using QuanTAlib;
|
||||
|
||||
// 1. Setup a source (publisher)
|
||||
var source = new TSeries();
|
||||
|
||||
// 2. Create indicator subscribed to source
|
||||
// It waits for events from 'source'
|
||||
var trima = new Trima(source, period: 14);
|
||||
|
||||
// 3. Optional: Subscribe to indicator's output
|
||||
trima.Pub += (item) => Console.WriteLine($"TRIMA Updated: {item.Value}");
|
||||
|
||||
// 4. Ingest data into source
|
||||
// This triggers the chain: source -> trima -> Console.WriteLine
|
||||
source.Add(new TValue(DateTime.Now, 100));
|
||||
source.Add(new TValue(DateTime.Now, 105));
|
||||
```
|
||||
|
||||
This pattern allows building complex, reactive processing pipelines without manual update loops.
|
||||
|
||||
## Interpretation Details
|
||||
|
||||
TRIMA can be used in various trading strategies:
|
||||
|
||||
* **Trend identification:** The direction of TRIMA indicates the prevailing trend
|
||||
* **Signal generation:** Crossovers between price and TRIMA generate trade signals with fewer false alarms than SMA
|
||||
* **Support/resistance levels:** TRIMA can act as dynamic support during uptrends and resistance during downtrends
|
||||
* **Trend strength assessment:** Distance between price and TRIMA can indicate trend strength
|
||||
* **Multiple timeframe analysis:** Using TRIMAs with different periods can confirm trends across different timeframes
|
||||
|
||||
## Limitations and Considerations
|
||||
|
||||
* **Market conditions:** Like all moving averages, less effective in choppy, sideways markets
|
||||
* **Lag factor:** More lag than WMA or EMA due to center-weighted emphasis
|
||||
* **Limited adaptability:** Fixed weighting scheme cannot adapt to changing market volatility
|
||||
* **Response time:** Takes longer to reflect sudden price changes than directionally-weighted averages
|
||||
* **Complementary tools:** Best used with momentum oscillators or volume indicators for confirmation
|
||||
|
||||
## References
|
||||
|
||||
* Ehlers, John F. "Cycle Analytics for Traders." Wiley, 2013
|
||||
* Kaufman, Perry J. "Trading Systems and Methods." Wiley, 2013
|
||||
* Colby, Robert W. "The Encyclopedia of Technical Market Indicators." McGraw-Hill, 2002
|
||||
Reference in New Issue
Block a user